From 71ee7c3aaea60a937ee8ef7f966b0e0d540c9f2f Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Tue, 11 Aug 2026 14:02:46 +0200 Subject: [PATCH] Update passwordless GDM backport patch series Add required pre-changes and update post-changes. Also, update generated style. Resolves: https://redhat.atlassian.net/browse/RHEL-236554 --- ...web-login-and-unified-auth-mechanism.patch | 9645 ++++++++++++++--- 0001-Update-generated-stylesheets.patch | 277 +- gnome-shell.spec | 7 +- ...hanges-for-passwordless-gdm-backport.patch | 4378 +++++--- ...hanges-for-passwordless-gdm-backport.patch | 146 +- 5 files changed, 11185 insertions(+), 3268 deletions(-) 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 2af0bc9..94891d1 100644 --- a/0001-Support-for-web-login-and-unified-auth-mechanism.patch +++ b/0001-Support-for-web-login-and-unified-auth-mechanism.patch @@ -1,59 +1,52 @@ -From 80b45dd1dbea84a6930c5a4a488b2cfdd28e62dd Mon Sep 17 00:00:00 2001 +From 0e444b337684a653fadb718f348909012d696cc8 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Thu, 2 Oct 2025 10:59:57 +0200 -Subject: [PATCH 01/48] style: Add common login dialog button styles to avoid +Subject: [PATCH 01/73] 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: -- -2.51.0 +2.55.0 -From f5323cccc3f0d454eed8e5a93b2faea78d783857 Mon Sep 17 00:00:00 2001 +From a6a7b52c0e1987e79672aedb0728c3fd9f0f74a0 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Thu, 12 Feb 2026 16:37:08 +0100 -Subject: [PATCH 02/48] unlockDialog: Vertically center dialog using fixed +Subject: [PATCH 02/73] unlockDialog: Vertically center dialog using fixed height -Use a fixed estimated height for centering so the position stays -stable regardless of actual content height changes during user -interaction. +Position the topY of the content based on a fixed prompt height estimation. -Ensure clock is always on the same position adding vertically center alignment. +With this fixed height, shorter elements (like UnlockDialogClock) are +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: --- - js/ui/unlockDialog.js | 26 ++++++++++++++++++++------ - 1 file changed, 20 insertions(+), 6 deletions(-) + js/ui/unlockDialog.js | 10 ++++++---- + 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/js/ui/unlockDialog.js b/js/ui/unlockDialog.js -index 169043b2a6..3f8ddec651 100644 +index 169043b2a..06b539c88 100644 --- a/js/ui/unlockDialog.js +++ b/js/ui/unlockDialog.js -@@ -27,6 +27,8 @@ const BLUR_SIGMA = 60; +@@ -25,6 +25,8 @@ const FADE_OUT_SCALE = 0.3; + const BLUR_BRIGHTNESS = 0.55; + const BLUR_SIGMA = 60; ++const FIXED_PROMPT_HEIGHT = 550; ++ const SUMMARY_ICON_SIZE = 32; -+const FIXED_PROMPT_HEIGHT = 400; -+ var NotificationsBox = GObject.registerClass({ - Signals: { 'wake-up-screen': {} }, - }, class NotificationsBox extends St.BoxLayout { -@@ -326,7 +328,11 @@ var NotificationsBox = GObject.registerClass({ - var Clock = GObject.registerClass( - class UnlockDialogClock extends St.BoxLayout { - _init() { -- super._init({ style_class: 'unlock-dialog-clock', vertical: true }); -+ super._init({ -+ style_class: 'unlock-dialog-clock', -+ orientation: Clutter.Orientation.VERTICAL, -+ y_align: Clutter.ActorAlign.CENTER, -+ }); - - this._time = new St.Label({ - style_class: 'unlock-dialog-clock-time', -@@ -417,8 +423,8 @@ class UnlockDialogLayout extends Clutter.LayoutManager { +@@ -417,8 +419,8 @@ class UnlockDialogLayout extends Clutter.LayoutManager { vfunc_allocate(container, box) { let [width, height] = box.get_size(); @@ -64,48 +57,40 @@ index 169043b2a6..3f8ddec651 100644 let [, , stackWidth, stackHeight] = this._stack.get_preferred_size(); -@@ -444,9 +450,17 @@ class UnlockDialogLayout extends Clutter.LayoutManager { +@@ -444,8 +446,8 @@ class UnlockDialogLayout extends Clutter.LayoutManager { this._notifications.allocate(actorBox); // Authentication Box - let stackY = Math.min( - thirdOfHeight, -- height - stackHeight - maxNotificationsHeight); -+ const dialog = container.get_parent(); -+ let stackY; -+ if (dialog._activePage === dialog._clock) { -+ stackY = Math.min( -+ Math.floor(centerY - stackHeight / 2.0), -+ height - stackHeight - maxNotificationsHeight); -+ } else { -+ stackY = Math.min( -+ Math.floor(centerY - FIXED_PROMPT_HEIGHT / 2.0), -+ height - stackHeight - maxNotificationsHeight); -+ } ++ const stackY = Math.min( ++ Math.floor(centerY - FIXED_PROMPT_HEIGHT / 2.0), + height - stackHeight - maxNotificationsHeight); actorBox.x1 = columnX1; - actorBox.y1 = stackY; -- -2.51.0 +2.55.0 -From 5cc8c69402a35b9c14c1db1025db5b4f681a261d Mon Sep 17 00:00:00 2001 +From cd5f3b8fbaf7db1f9ae1d4f87d71516cb735d9b1 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Thu, 12 Feb 2026 16:38:04 +0100 -Subject: [PATCH 03/48] unlockDialog: Fix username reuse on reset +Subject: [PATCH 03/73] 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(-) diff --git a/js/ui/unlockDialog.js b/js/ui/unlockDialog.js -index 3f8ddec651..753c070bce 100644 +index 06b539c88..9d00f3a55 100644 --- a/js/ui/unlockDialog.js +++ b/js/ui/unlockDialog.js -@@ -802,7 +802,7 @@ var UnlockDialog = GObject.registerClass({ +@@ -790,7 +790,7 @@ var UnlockDialog = GObject.registerClass({ _onReset(authPrompt, beginRequest) { let userName; @@ -115,27 +100,29 @@ index 3f8ddec651..753c070bce 100644 userName = this._userName; } else { -- -2.51.0 +2.55.0 -From 1a7adb6d93257b78e5fc8ab711fd781ed93a9dce Mon Sep 17 00:00:00 2001 +From 1573b79d5770d28747399a2e41361f0c340d0740 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Thu, 12 Feb 2026 16:38:54 +0100 -Subject: [PATCH 04/48] unlockDialog: Wait for authPrompt destruction before +Subject: [PATCH 04/73] 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(-) diff --git a/js/ui/unlockDialog.js b/js/ui/unlockDialog.js -index 753c070bce..3bd40c92ac 100644 +index 9d00f3a55..569feeaf7 100644 --- a/js/ui/unlockDialog.js +++ b/js/ui/unlockDialog.js -@@ -852,8 +852,7 @@ var UnlockDialog = GObject.registerClass({ +@@ -840,8 +840,7 @@ var UnlockDialog = GObject.registerClass({ } _otherUserClicked() { @@ -146,30 +133,19 @@ index 753c070bce..3bd40c92ac 100644 } -- -2.51.0 +2.55.0 -From 52e474166a0871545852ce63a1ca667d1f4ad9dd Mon Sep 17 00:00:00 2001 -From: Joan Torres Lopez -Date: Tue, 10 Mar 2026 13:56:02 +0100 -Subject: [PATCH 05/48] 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. --- -2.51.0 - - -From fc957c7f2c984cfbb48a648f7143e4de772f2489 Mon Sep 17 00:00:00 2001 +From 2f893f918110afd536e49fcbb6c867cab9a19182 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Thu, 5 Feb 2026 18:55:38 +0100 -Subject: [PATCH 06/48] authPrompt: Use destructured object for +Subject: [PATCH 05/73] 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 +++--- @@ -177,7 +153,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 f9205d41dd..f048aa341e 100644 +index 11f34fc2c..493010632 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js @@ -285,7 +285,7 @@ var AuthPrompt = GObject.registerClass({ @@ -207,7 +183,7 @@ index f9205d41dd..f048aa341e 100644 this.emit('prompted'); } -@@ -404,7 +404,7 @@ var AuthPrompt = GObject.registerClass({ +@@ -417,7 +417,7 @@ var AuthPrompt = GObject.registerClass({ this.clear(); } @@ -216,7 +192,7 @@ index f9205d41dd..f048aa341e 100644 this.setActorInDefaultButtonWell(null); if (!canRetry) -@@ -525,12 +525,12 @@ var AuthPrompt = GObject.registerClass({ +@@ -538,12 +538,12 @@ var AuthPrompt = GObject.registerClass({ opacity: 0, visible: true, }); @@ -231,8 +207,8 @@ index f9205d41dd..f048aa341e 100644 }); } -@@ -605,7 +605,7 @@ var AuthPrompt = GObject.registerClass({ - } +@@ -608,7 +608,7 @@ var AuthPrompt = GObject.registerClass({ + Util.wiggle(this._message, wiggleParameters); } - updateSensitivity(sensitive) { @@ -240,7 +216,7 @@ index f9205d41dd..f048aa341e 100644 let authWidget; if (this._authList.visible) -@@ -635,7 +635,7 @@ var AuthPrompt = GObject.registerClass({ +@@ -638,7 +638,7 @@ var AuthPrompt = GObject.registerClass({ this.setUser(null); @@ -249,7 +225,7 @@ index f9205d41dd..f048aa341e 100644 this._entry.set_text(''); } -@@ -728,7 +728,7 @@ var AuthPrompt = GObject.registerClass({ +@@ -731,7 +731,7 @@ var AuthPrompt = GObject.registerClass({ params = Params.parse(params, { userName: null, hold: null }); @@ -259,7 +235,7 @@ index f9205d41dd..f048aa341e 100644 let hold = params.hold; if (!hold) diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js -index 8ed0a0d322..96999d7e6f 100644 +index 8ed0a0d32..96999d7e6 100644 --- a/js/gdm/loginDialog.js +++ b/js/gdm/loginDialog.js @@ -947,8 +947,8 @@ var LoginDialog = GObject.registerClass({ @@ -283,10 +259,10 @@ index 8ed0a0d322..96999d7e6f 100644 } diff --git a/js/ui/unlockDialog.js b/js/ui/unlockDialog.js -index 3bd40c92ac..19df7fc7b2 100644 +index 569feeaf7..6e69cd079 100644 --- a/js/ui/unlockDialog.js +++ b/js/ui/unlockDialog.js -@@ -719,7 +719,7 @@ var UnlockDialog = GObject.registerClass({ +@@ -707,7 +707,7 @@ var UnlockDialog = GObject.registerClass({ case AuthPrompt.AuthPromptStatus.VERIFICATION_FAILED: this._authPrompt.reset(); this._authPrompt.updateSensitivity( @@ -296,26 +272,28 @@ index 3bd40c92ac..19df7fc7b2 100644 } -- -2.51.0 +2.55.0 -From 326e650dddb72c20b69a29748cf05a5ee1205699 Mon Sep 17 00:00:00 2001 +From 8255960c0d63acc2cd28eac61fdfce6939c9ac1e Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Thu, 12 Feb 2026 16:47:22 +0100 -Subject: [PATCH 07/48] authPrompt: Use array-based widget lookup in +Subject: [PATCH 06/73] 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 f048aa341e..868d3a592d 100644 +index 493010632..dae36c22d 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -606,12 +606,9 @@ var AuthPrompt = GObject.registerClass({ +@@ -609,12 +609,9 @@ var AuthPrompt = GObject.registerClass({ } updateSensitivity({sensitive}) { @@ -332,27 +310,31 @@ index f048aa341e..868d3a592d 100644 if (authWidget.reactive === sensitive) return; -- -2.51.0 +2.55.0 -From c59a7bccea69a6e2ed9bbd5e8c858807523038cc Mon Sep 17 00:00:00 2001 +From 36cb8a3e5bfb814023093db9473d4dcdc3b3e793 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Tue, 10 Mar 2026 17:10:58 +0100 -Subject: [PATCH 08/48] authPrompt: Generalize _fadeInChoiceList to accept any +Subject: [PATCH 07/73] 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 868d3a592d..b76326c490 100644 +index dae36c22d..97ca5aca3 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -520,13 +520,16 @@ var AuthPrompt = GObject.registerClass({ +@@ -533,17 +533,18 @@ var AuthPrompt = GObject.registerClass({ this._entry.grab_key_focus(); } @@ -366,42 +348,50 @@ index 868d3a592d..b76326c490 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, -@@ -545,7 +548,7 @@ var AuthPrompt = GObject.registerClass({ +- transition: Clutter.AnimationMode.EASE_OUT_QUAD, +- onComplete: () => this.updateSensitivity({sensitive: true}), ++ mode: Clutter.AnimationMode.EASE_OUT_QUAD, + }); + } + +@@ -558,7 +559,8 @@ var 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.51.0 +2.55.0 -From 1818770978fe025456807dbf0122f6faebc16f46 Mon Sep 17 00:00:00 2001 +From ae46d489da389f2c6e78876fe29b5371470018cf Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Fri, 9 Feb 2024 09:02:25 -0500 -Subject: [PATCH 09/48] authPrompt: Fade out input buttons/entry after +Subject: [PATCH 08/73] 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 b76326c490..613c594a94 100644 +index 97ca5aca3..d92162203 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js @@ -50,6 +50,7 @@ var AuthPrompt = GObject.registerClass({ @@ -412,7 +402,7 @@ index b76326c490..613c594a94 100644 }, }, class AuthPrompt extends St.BoxLayout { _init(gdmClient, mode) { -@@ -417,8 +418,16 @@ var AuthPrompt = GObject.registerClass({ +@@ -430,8 +431,15 @@ var AuthPrompt = GObject.registerClass({ _onVerificationComplete() { this.setActorInDefaultButtonWell(null); this.verificationStatus = AuthPromptStatus.VERIFICATION_SUCCEEDED; @@ -420,7 +410,6 @@ index b76326c490..613c594a94 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, @@ -431,19 +420,18 @@ index b76326c490..613c594a94 100644 } _onReset() { -@@ -505,6 +514,10 @@ var AuthPrompt = GObject.registerClass({ +@@ -518,6 +526,9 @@ var 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 96999d7e6f..bc62416e88 100644 +index 96999d7e6..dd4123fbb 100644 --- a/js/gdm/loginDialog.js +++ b/js/gdm/loginDialog.js @@ -451,6 +451,7 @@ var LoginDialog = GObject.registerClass({ @@ -454,13 +442,12 @@ index 96999d7e6f..bc62416e88 100644 this._authPrompt.hide(); this.add_child(this._authPrompt); -@@ -890,6 +891,16 @@ var LoginDialog = GObject.registerClass({ +@@ -890,6 +891,15 @@ var LoginDialog = GObject.registerClass({ } } + _onVerificationComplete() { + this._bottomButtonGroup.reactive = false; -+ this._bottomButtonGroup.can_focus = false; + this._bottomButtonGroup.ease({ + opacity: 0, + duration: _FADE_ANIMATION_TIME, @@ -472,13 +459,13 @@ index 96999d7e6f..bc62416e88 100644 this._sessionMenuButton.setActiveSession(sessionId); } -- -2.51.0 +2.55.0 -From dcbfeceb6ec5cb6be19530e303908a00018f5e3d Mon Sep 17 00:00:00 2001 +From 25c5914fc3e2cd0b14b64cb9b0561660b3a2e5ae Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Wed, 21 Jan 2026 14:23:34 +0100 -Subject: [PATCH 10/48] authPrompt: Don't reset preemptiveAnswer when +Subject: [PATCH 09/73] authPrompt: Don't reset preemptiveAnswer when VERIFICATION_IN_PROGRESS PreemptiveAnswer wasn't being used in the case where verification is in @@ -486,12 +473,14 @@ 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 613c594a94..1b6f67035a 100644 +index d92162203..39085609c 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js @@ -682,7 +682,8 @@ var AuthPrompt = GObject.registerClass({ @@ -505,22 +494,24 @@ index 613c594a94..1b6f67035a 100644 if (this._preemptiveAnswerWatchId) this._idleMonitor.remove_watch(this._preemptiveAnswerWatchId); -- -2.51.0 +2.55.0 -From 74d754e9ce157599449d73838688aab698d257e9 Mon Sep 17 00:00:00 2001 +From 5a0ebac8c2cfab71cc7579f7d4ec72525889371a Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Thu, 12 Feb 2026 17:05:32 +0100 -Subject: [PATCH 11/48] style: Increase hint-text left margin +Subject: [PATCH 10/73] 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(-) diff --git a/data/theme/gnome-shell-sass/widgets/_entries.scss b/data/theme/gnome-shell-sass/widgets/_entries.scss -index 0a43e86f37..d76d17f282 100644 +index 0a43e86f3..d76d17f28 100644 --- a/data/theme/gnome-shell-sass/widgets/_entries.scss +++ b/data/theme/gnome-shell-sass/widgets/_entries.scss @@ -21,7 +21,7 @@ StEntry { @@ -533,23 +524,25 @@ index 0a43e86f37..d76d17f282 100644 } } -- -2.51.0 +2.55.0 -From a3e17c6c7e0fc5af4232e871fd8c9011b2baf190 Mon Sep 17 00:00:00 2001 +From 203ca2d4078b3df3f373e5fc11f9cb2a438d50c5 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Mon, 16 Feb 2026 16:05:17 +0100 -Subject: [PATCH 12/48] authPrompt: Use connectObject for userVerifier signals +Subject: [PATCH 11/73] 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 | 19 +++++++++++-------- - 1 file changed, 11 insertions(+), 8 deletions(-) + js/gdm/authPrompt.js | 27 +++++++++++++++------------ + 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index 1b6f67035a..c26b98c024 100644 +index 39085609c..e861c018f 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js @@ -78,14 +78,16 @@ var AuthPrompt = GObject.registerClass({ @@ -565,14 +558,14 @@ index 1b6f67035a..c26b98c024 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; @@ -585,23 +578,61 @@ index 1b6f67035a..c26b98c024 100644 this._userVerifier.destroy(); this._userVerifier = null; this._entry = null; +@@ -328,7 +331,7 @@ var AuthPrompt = GObject.registerClass({ + this._capsLockWarningLabel.visible = secret; + } + +- _onAskQuestion(verifier, serviceName, question, secret) { ++ _onAskQuestion(serviceName, question, secret) { + if (this._queryingService) + this.clear(); + +@@ -353,7 +356,7 @@ var AuthPrompt = GObject.registerClass({ + this.emit('prompted'); + } + +- _onShowChoiceList(userVerifier, serviceName, promptMessage, choiceList) { ++ _onShowChoiceList(serviceName, promptMessage, choiceList) { + if (this._queryingService) + this.clear(); + +@@ -392,7 +395,7 @@ var AuthPrompt = GObject.registerClass({ + this.reset(); + } + +- _onShowMessage(_userVerifier, serviceName, message, type) { ++ _onShowMessage(serviceName, message, type) { + let wiggleParameters = {duration: 0}; + + if (type === GdmUtil.MessageType.ERROR && +@@ -410,7 +413,7 @@ var AuthPrompt = GObject.registerClass({ + this.emit('prompted'); + } + +- _onVerificationFailed(userVerifier, serviceName, canRetry) { ++ _onVerificationFailed(serviceName, canRetry) { + const wasQueryingService = this._queryingService === serviceName; + + if (wasQueryingService) { -- -2.51.0 +2.55.0 -From 21e361883a579b235b9076e774156bd42ea888ac Mon Sep 17 00:00:00 2001 +From 50ec963f0aea925d39b3343361c784467e4573d4 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Thu, 12 Feb 2026 17:09:44 +0100 -Subject: [PATCH 13/48] authPrompt: Group animation constants together +Subject: [PATCH 12/73] 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 c26b98c024..fa0fb8f5af 100644 +index e861c018f..d08cb8465 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js @@ -17,7 +17,6 @@ const Util = imports.misc.util; @@ -613,86 +644,37 @@ index c26b98c024..fa0fb8f5af 100644 const LOCKDOWN_SCHEMA = 'org.gnome.desktop.lockdown'; -- -2.51.0 +2.55.0 -From db22c1b01dc865aef97c79d2558aac09285de678 Mon Sep 17 00:00:00 2001 +From 9746f3b6c5ff1562aa6981c504ec50dcdae53bfc Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Thu, 12 Feb 2026 17:20:34 +0100 -Subject: [PATCH 14/48] authPrompt: Update entry layout based on mockups +Subject: [PATCH 13/73] authPrompt: Update entry layout based on mockups Implement the new authentication prompt layout based on the design mockups at: https://gitlab.gnome.org/Teams/Design/os-mockups/-/blob/master/lock-login/passwordless-login-aday.png -The entry is now larger and more rounded. The entry area has a richer -UI with the password visibility toggle (already present) and a new -_defaultButtonWell inside it that can show either a next button -(default) or a spinner when authentication is in progress. +The entry is now larger and more rounded. The next button and the +spinner appear inside the entry. -To achieve this, _entry and _defaultButtonWell are wrapped in a new -_entryArea container with a BinLayout, allowing _defaultButtonWell -to overlay _entry aligned to the right. +The entry is horizontally centered as username and avatar. The +cancelButton is placed on the left of the centered entry. -The _mainBox now uses a BinLayout to allow it to be horizontally centered -below the username and avatar. This also enables constraining -the cancelButton to the left edge of _mainBox using an AlignConstraint -on the X axis, so it appears offset to the left without affecting -the horizontal center of _mainBox. +Part-of: --- - js/gdm/authPrompt.js | 69 ++++++++++++++++++++++++++++++++------------ - 1 file changed, 51 insertions(+), 18 deletions(-) + js/gdm/authPrompt.js | 62 +++++++++++++++++++++++++++++++++----------- + 1 file changed, 47 insertions(+), 15 deletions(-) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index fa0fb8f5af..7e9469391d 100644 +index d08cb8465..63491e85e 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -1,7 +1,7 @@ - // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- - /* exported AuthPrompt */ - --const { Clutter, Gio, GLib, GObject, Meta, Pango, Shell, St } = imports.gi; -+const { Clutter, Gio, GLib, GObject, Graphene, Meta, Pango, Shell, St } = imports.gi; - - const Animation = imports.ui.animation; - const AuthList = imports.gdm.authList; -@@ -146,9 +146,11 @@ var AuthPrompt = GObject.registerClass({ - } - - _initInputRow() { -- this._mainBox = new St.BoxLayout({ -+ this._mainBox = new St.Widget({ -+ layout_manager: new Clutter.BinLayout(), - style_class: 'login-dialog-button-box', -- vertical: false, -+ x_expand: true, -+ y_expand: false, - }); - this.add_child(this._mainBox); - -@@ -158,10 +160,17 @@ var AuthPrompt = GObject.registerClass({ - button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE, - reactive: this._hasCancelButton, - can_focus: this._hasCancelButton, -+ x_expand: true, - x_align: Clutter.ActorAlign.START, - y_align: Clutter.ActorAlign.CENTER, - child: new St.Icon({ icon_name: 'go-previous-symbolic' }), - }); -+ this.cancelButton.add_constraint(new Clutter.AlignConstraint({ -+ source: this._mainBox, -+ align_axis: Clutter.AlignAxis.X_AXIS, -+ pivot_point: new Graphene.Point({x: 1, y: 0}), -+ })); -+ - if (this._hasCancelButton) - this.cancelButton.connect('clicked', () => this.cancel()); - else -@@ -187,10 +196,20 @@ var AuthPrompt = GObject.registerClass({ +@@ -187,10 +187,20 @@ var 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(), @@ -702,7 +684,7 @@ index fa0fb8f5af..7e9469391d 100644 + }); + this._mainBox.add_child(this._entryArea); + -+ const entryParams = { + let entryParams = { style_class: 'login-dialog-prompt-entry', can_focus: true, x_expand: true, @@ -710,17 +692,16 @@ index fa0fb8f5af..7e9469391d 100644 }; this._entry = null; -@@ -202,8 +221,7 @@ var AuthPrompt = GObject.registerClass({ +@@ -202,7 +212,7 @@ var AuthPrompt = GObject.registerClass({ ShellEntry.addContextMenu(this._passwordEntry, { actionMode: Shell.ActionMode.NONE }); this._entry = this._passwordEntry; - this._mainBox.add_child(this._entry); -- this._entry.grab_key_focus(); + this._entryArea.add_child(this._entry); + this._entry.grab_key_focus(); this._lockdownSettings = new Gio.Settings({ schema_id: LOCKDOWN_SCHEMA }); - this._lockdownSettings.connect(`changed::${DISABLE_SHOW_PASSWORD_KEY}`, -@@ -232,17 +250,28 @@ var AuthPrompt = GObject.registerClass({ +@@ -232,17 +242,38 @@ var AuthPrompt = GObject.registerClass({ this._defaultButtonWell = new St.Widget({ layout_manager: new Clutter.BinLayout(), @@ -738,6 +719,7 @@ index fa0fb8f5af..7e9469391d 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, @@ -751,10 +733,19 @@ index fa0fb8f5af..7e9469391d 100644 this._defaultButtonWell.add_child(this._spinner); + + 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 Clutter.Actor({ ++ constraints: new Clutter.BindConstraint({ ++ source: this.cancelButton, ++ coordinate: Clutter.BindCoordinate.WIDTH, ++ }), ++ })); } _updateShowPasswordIcon() { -@@ -319,7 +348,7 @@ var AuthPrompt = GObject.registerClass({ +@@ -319,7 +350,7 @@ var AuthPrompt = GObject.registerClass({ } if (newEntry) { @@ -763,7 +754,7 @@ index fa0fb8f5af..7e9469391d 100644 this._entry = newEntry; this._inactiveEntry = inactiveEntry; -@@ -408,17 +437,17 @@ var AuthPrompt = GObject.registerClass({ +@@ -421,17 +452,17 @@ var AuthPrompt = GObject.registerClass({ } this.updateSensitivity({sensitive: canRetry}); @@ -784,7 +775,7 @@ index fa0fb8f5af..7e9469391d 100644 this.verificationStatus = AuthPromptStatus.VERIFICATION_SUCCEEDED; this._mainBox.reactive = false; -@@ -531,7 +560,8 @@ var AuthPrompt = GObject.registerClass({ +@@ -542,7 +573,8 @@ var AuthPrompt = GObject.registerClass({ this._entry.hint_text = question; this._authList.hide(); @@ -794,7 +785,7 @@ index fa0fb8f5af..7e9469391d 100644 this._entry.grab_key_focus(); } -@@ -560,7 +590,7 @@ var AuthPrompt = GObject.registerClass({ +@@ -569,7 +601,7 @@ var AuthPrompt = GObject.registerClass({ this._authList.addItem(key, text); } @@ -803,17 +794,18 @@ index fa0fb8f5af..7e9469391d 100644 if (this._message.text === '') this._message.hide(); this._fadeInElement(this._authList); -@@ -631,6 +661,9 @@ var AuthPrompt = GObject.registerClass({ - if (authWidget.reactive === sensitive) - return; +@@ -628,8 +660,8 @@ var 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) { -@@ -644,7 +677,7 @@ var AuthPrompt = GObject.registerClass({ +@@ -644,7 +676,7 @@ var AuthPrompt = GObject.registerClass({ } vfunc_hide() { @@ -823,13 +815,13 @@ index fa0fb8f5af..7e9469391d 100644 this._message.opacity = 0; -- -2.51.0 +2.55.0 -From fc95866bf3faefd93e5b323876a6bb509dc7d54d Mon Sep 17 00:00:00 2001 +From 99f14dc9cf41afc76d2b9effb9aef84a6db58df5 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Thu, 12 Feb 2026 17:41:48 +0100 -Subject: [PATCH 15/48] authPrompt: Fade in _entryArea instead of abruptly +Subject: [PATCH 14/73] authPrompt: Fade in _entryArea instead of abruptly showing it To support the new fade-in flow, the _entryArea is now hidden by default. @@ -839,18 +831,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 7e9469391d..9503030d3b 100644 +index 63491e85e..6adc90b8b 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -380,7 +380,6 @@ var AuthPrompt = GObject.registerClass({ +@@ -382,7 +382,6 @@ var AuthPrompt = GObject.registerClass({ else this.setQuestion(question.replace(/: *$/, '').trim()); @@ -858,8 +849,8 @@ index 7e9469391d..9503030d3b 100644 this.emit('prompted'); } -@@ -431,10 +430,8 @@ var AuthPrompt = GObject.registerClass({ - _onVerificationFailed(userVerifier, serviceName, canRetry) { +@@ -446,10 +445,8 @@ var AuthPrompt = GObject.registerClass({ + _onVerificationFailed(serviceName, canRetry) { const wasQueryingService = this._queryingService === serviceName; - if (wasQueryingService) { @@ -870,7 +861,7 @@ index 7e9469391d..9503030d3b 100644 this.updateSensitivity({sensitive: canRetry}); this.setActorInDefaultButtonWell(this._nextButton); -@@ -540,6 +537,7 @@ var AuthPrompt = GObject.registerClass({ +@@ -554,6 +551,7 @@ var AuthPrompt = GObject.registerClass({ } clear() { @@ -878,77 +869,169 @@ index 7e9469391d..9503030d3b 100644 this._entry.text = ''; this._inactiveEntry.text = ''; this.stopSpinning(); -@@ -561,8 +559,7 @@ var AuthPrompt = GObject.registerClass({ +@@ -574,8 +572,8 @@ var 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 dd4123fbb..e8103fec8 100644 +--- a/js/gdm/loginDialog.js ++++ b/js/gdm/loginDialog.js +@@ -967,7 +967,6 @@ var LoginDialog = GObject.registerClass({ + this._updateCancelButton(); + + this._sessionMenuButton.updateSensitivity(false); +- this._authPrompt.updateSensitivity({sensitive: true}); + this._showPrompt(); + } + -- -2.51.0 +2.55.0 -From 64c603f3e601c271ddd32186943186c5a9f52bc7 Mon Sep 17 00:00:00 2001 +From fd3d6cce7175e345f48714e866d0e2d12f57a574 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Thu, 12 Feb 2026 17:36:08 +0100 -Subject: [PATCH 16/48] authPrompt: Show entry area when displaying message +Subject: [PATCH 15/73] 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 | 9 +++++++++ - 1 file changed, 9 insertions(+) + js/gdm/authPrompt.js | 10 ++++++++++ + 1 file changed, 10 insertions(+) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index 9503030d3b..2467362fe4 100644 +index 6adc90b8b..3347246bf 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -424,6 +424,15 @@ var AuthPrompt = GObject.registerClass({ +@@ -440,6 +440,16 @@ var AuthPrompt = GObject.registerClass({ - _onShowMessage(_userVerifier, serviceName, message, type) { - this.setMessage(serviceName, message, type); + this.setMessage(message, type, wiggleParameters); + 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.emit('prompted'); ++ this.updateSensitivity({sensitive: true}); ++ } } + _onVerificationFailed(serviceName, canRetry) { -- -2.51.0 +2.55.0 -From 88bbd0ee5ae2ce1b47a89d55fa9870145908c8ac Mon Sep 17 00:00:00 2001 +From 5f031e73bde57285200d889a6128c3b0511ea5f7 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez -Date: Thu, 12 Feb 2026 17:28:54 +0100 -Subject: [PATCH 17/48] authPrompt: Refactor button-well animation and add - loading signal +Date: Tue, 19 May 2026 10:21:50 +0200 +Subject: [PATCH 16/73] authPrompt: Add loading signal -- Add start/stop spinning() methods. -- Add 'loading' signal emitted on start/stop spinning (used in upcoming - commits). -- Replace setActorInDefaultButtonWell() with start/stop spinning() - methods. -- Simplify _activateNext checking internally reactive state and - passwordEntry. -- Simplify setActorInDefaultButtonWell and remove unused - DEFAULT_BUTTON_WELL_ANIMATION_DELAY constant. +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 | 113 +++++++++++++++++++------------------------ - 1 file changed, 50 insertions(+), 63 deletions(-) + 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 2467362fe4..370a0601e8 100644 +index 3347246bf..56eb01c9b 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -50,6 +50,7 @@ var AuthPrompt = GObject.registerClass({ + 'prompted': {}, + 'reset': { param_types: [GObject.TYPE_UINT] }, + 'verification-complete': {}, ++ 'loading': {param_types: [GObject.TYPE_BOOLEAN]}, + }, + }, class AuthPrompt extends St.BoxLayout { + _init(gdmClient, mode) { +@@ -459,7 +460,7 @@ var AuthPrompt = GObject.registerClass({ + this._queryingService = null; + + this.updateSensitivity({sensitive: canRetry}); +- this.setActorInDefaultButtonWell(this._nextButton); ++ this.stopSpinning(); + + if (!canRetry) + this.verificationStatus = AuthPromptStatus.VERIFICATION_FAILED; +@@ -469,7 +470,7 @@ var AuthPrompt = GObject.registerClass({ + } + + _onVerificationComplete() { +- this.setActorInDefaultButtonWell(this._nextButton, true); ++ this.stopSpinning({animate: true}); + this.verificationStatus = AuthPromptStatus.VERIFICATION_SUCCEEDED; + + this._mainBox.reactive = false; +@@ -552,12 +553,14 @@ var AuthPrompt = GObject.registerClass({ + this._defaultButtonWellActor = actor; + } + +- startSpinning() { +- this.setActorInDefaultButtonWell(this._spinner, true); ++ startSpinning({animate = false} = {}) { ++ this.emit('loading', true); ++ this.setActorInDefaultButtonWell(this._spinner, animate); + } + +- stopSpinning() { +- this.setActorInDefaultButtonWell(null, false); ++ stopSpinning({animate = false} = {}) { ++ this.emit('loading', false); ++ this.setActorInDefaultButtonWell(this._nextButton, animate); + } + + clear() { +@@ -684,7 +687,7 @@ var AuthPrompt = GObject.registerClass({ + } + + vfunc_hide() { +- this.setActorInDefaultButtonWell(this._nextButton, true); ++ this.stopSpinning(); + super.vfunc_hide(); + this._message.opacity = 0; + +-- +2.55.0 + + +From ea95eef95edb93d228e70ab1b9c8f11d6742621a Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Tue, 19 May 2026 10:24:38 +0200 +Subject: [PATCH 17/73] 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 +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 56eb01c9b..a1eb3af6f 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js @@ -15,7 +15,6 @@ const UserWidget = imports.ui.userWidget; @@ -959,15 +1042,42 @@ index 2467362fe4..370a0601e8 100644 var DEFAULT_BUTTON_WELL_ANIMATION_TIME = 300; var MESSAGE_FADE_OUT_ANIMATION_TIME = 500; -@@ -50,6 +49,7 @@ var AuthPrompt = GObject.registerClass({ - 'prompted': {}, - 'reset': { param_types: [GObject.TYPE_UINT] }, - 'verification-complete': {}, -+ 'loading': {param_types: [GObject.TYPE_BOOLEAN]}, - }, - }, class AuthPrompt extends St.BoxLayout { - _init(gdmClient, mode) { -@@ -241,11 +241,7 @@ var AuthPrompt = GObject.registerClass({ +@@ -522,7 +521,6 @@ var AuthPrompt = GObject.registerClass({ + oldActor.ease({ + opacity: 0, + duration: DEFAULT_BUTTON_WELL_ANIMATION_TIME, +- delay: DEFAULT_BUTTON_WELL_ANIMATION_DELAY, + mode: Clutter.AnimationMode.LINEAR, + onComplete: () => { + if (wasSpinner) { +@@ -544,7 +542,7 @@ var AuthPrompt = GObject.registerClass({ + actor.ease({ + opacity: 255, + duration: DEFAULT_BUTTON_WELL_ANIMATION_TIME, +- delay: DEFAULT_BUTTON_WELL_ANIMATION_DELAY, ++ delay: oldActor ? DEFAULT_BUTTON_WELL_ANIMATION_TIME : 0, + mode: Clutter.AnimationMode.LINEAR, + }); + } +-- +2.55.0 + + +From 7e096094430b2a0181ad9f4c7f5bdbb71dd271ce Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Thu, 12 Feb 2026 17:28:54 +0100 +Subject: [PATCH 18/73] authPrompt: Simplify some code + +Part-of: +--- + js/gdm/authPrompt.js | 96 +++++++++++++++++++------------------------- + 1 file changed, 41 insertions(+), 55 deletions(-) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index a1eb3af6f..047b2b4f6 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -233,11 +233,7 @@ var AuthPrompt = GObject.registerClass({ this._fadeOutMessage(); }); @@ -980,7 +1090,7 @@ index 2467362fe4..370a0601e8 100644 }); this._defaultButtonWell = new St.Widget({ -@@ -315,13 +311,16 @@ var AuthPrompt = GObject.registerClass({ +@@ -317,14 +313,17 @@ var AuthPrompt = GObject.registerClass({ this._timedLoginIndicator.scale_x = 0.; } @@ -992,33 +1102,17 @@ index 2467362fe4..370a0601e8 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 { -@@ -443,7 +442,7 @@ var AuthPrompt = GObject.registerClass({ - this._queryingService = null; - - this.updateSensitivity({sensitive: canRetry}); -- this.setActorInDefaultButtonWell(this._nextButton); -+ this.stopSpinning(); - - if (!canRetry) - this.verificationStatus = AuthPromptStatus.VERIFICATION_FAILED; -@@ -453,7 +452,7 @@ var AuthPrompt = GObject.registerClass({ - } - - _onVerificationComplete() { -- this.setActorInDefaultButtonWell(this._nextButton, true); -+ this.stopSpinning({animate: true}); - this.verificationStatus = AuthPromptStatus.VERIFICATION_SUCCEEDED; - - this._mainBox.reactive = false; -@@ -473,76 +472,64 @@ var AuthPrompt = GObject.registerClass({ + this._preemptiveAnswer = this._entry.text; +@@ -488,64 +487,51 @@ var AuthPrompt = GObject.registerClass({ } setActorInDefaultButtonWell(actor, animate) { @@ -1039,15 +1133,15 @@ index 2467362fe4..370a0601e8 100644 - wasSpinner = true; - else - wasSpinner = false; -+ if (actor === this._spinner) -+ this._spinner.play(); - +- - let isSpinner; - if (actor == this._spinner) - isSpinner = true; - else - isSpinner = false; -- ++ if (actor === this._spinner) ++ this._spinner.play(); + - if (this._defaultButtonWellActor != actor && oldActor) { - if (!animate) { + if (!animate) { @@ -1062,7 +1156,6 @@ index 2467362fe4..370a0601e8 100644 - oldActor.ease({ - opacity: 0, - duration: DEFAULT_BUTTON_WELL_ANIMATION_TIME, -- delay: DEFAULT_BUTTON_WELL_ANIMATION_DELAY, - mode: Clutter.AnimationMode.LINEAR, - onComplete: () => { - if (wasSpinner) { @@ -1103,7 +1196,7 @@ index 2467362fe4..370a0601e8 100644 - actor.ease({ - opacity: 255, - duration: DEFAULT_BUTTON_WELL_ANIMATION_TIME, -- delay: DEFAULT_BUTTON_WELL_ANIMATION_DELAY, +- delay: oldActor ? DEFAULT_BUTTON_WELL_ANIMATION_TIME : 0, - mode: Clutter.AnimationMode.LINEAR, - }); - } @@ -1117,51 +1210,66 @@ index 2467362fe4..370a0601e8 100644 } this._defaultButtonWellActor = actor; - } - -- startSpinning() { -- this.setActorInDefaultButtonWell(this._spinner, true); -+ startSpinning({animate = false} = {}) { -+ this.emit('loading', true); -+ this.setActorInDefaultButtonWell(this._spinner, animate); - } - -- stopSpinning() { -- this.setActorInDefaultButtonWell(null, false); -+ stopSpinning({animate = false} = {}) { -+ this.emit('loading', false); -+ this.setActorInDefaultButtonWell(this._nextButton, animate); - } - - clear() { -@@ -683,7 +670,7 @@ var AuthPrompt = GObject.registerClass({ - } - - vfunc_hide() { -- this.setActorInDefaultButtonWell(this._nextButton, true); -+ this.stopSpinning(); - super.vfunc_hide(); - this._message.opacity = 0; - -- -2.51.0 +2.55.0 -From bcff8a5091b13cd0f0e2555ef52e27b3a028cb62 Mon Sep 17 00:00:00 2001 +From 47fd58d09dd15bdd5c1e649beec7279bec4ea4f4 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Tue, 26 May 2026 16:48:43 +0200 +Subject: [PATCH 19/73] 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 047b2b4f6..1a7d6e682 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -597,8 +597,6 @@ var AuthPrompt = GObject.registerClass({ + } + + this._entryArea.hide(); +- if (this._message.text === '') +- this._message.hide(); + this._fadeInElement(this._authList); + this.updateSensitivity({sensitive: true}); + } +@@ -638,7 +636,6 @@ var AuthPrompt = GObject.registerClass({ + else + this._message.remove_style_class_name('login-dialog-message-hint'); + +- this._message.show(); + if (message) { + this._message.remove_all_transitions(); + this._message.text = message; +-- +2.55.0 + + +From 10748b56f72380d8667554b4364aa8091859c5a2 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Tue, 10 Mar 2026 18:32:48 +0100 -Subject: [PATCH 18/48] authPrompt: Skip reset after successful verification +Subject: [PATCH 20/73] 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 370a0601e8..a970f17ca9 100644 +index 1a7d6e682..0b4e07815 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -467,6 +467,9 @@ var AuthPrompt = GObject.registerClass({ +@@ -482,6 +482,9 @@ var AuthPrompt = GObject.registerClass({ } _onReset() { @@ -1172,33 +1280,44 @@ index 370a0601e8..a970f17ca9 100644 this.reset(); } -- -2.51.0 +2.55.0 -From 26aef7076824f3e50c1b3ffb9e7f3141f1ac826e Mon Sep 17 00:00:00 2001 +From 9e31a2668fc49f06bbe1ed4ef856e8d0c6b6f52d Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Tue, 6 Feb 2024 13:06:32 -0500 -Subject: [PATCH 19/48] authPrompt: Parameterize reset function +Subject: [PATCH 21/73] 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 | 32 ++++++++++++++++++++++---------- - 1 file changed, 22 insertions(+), 10 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 a970f17ca9..80c65eef90 100644 +index 0b4e07815..fa7fb2a43 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -466,12 +466,11 @@ var AuthPrompt = GObject.registerClass({ +@@ -83,7 +83,7 @@ var 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); +@@ -481,12 +481,11 @@ var AuthPrompt = GObject.registerClass({ this.emit('verification-complete'); } - _onReset() { -+ _onReset(_userVerifier, resetParams) { ++ _onReset(resetParams) { if (this.verificationStatus === AuthPromptStatus.VERIFICATION_SUCCEEDED) return; @@ -1208,28 +1327,31 @@ index a970f17ca9..80c65eef90 100644 } setActorInDefaultButtonWell(actor, animate) { -@@ -535,10 +534,17 @@ var AuthPrompt = GObject.registerClass({ +@@ -550,11 +549,18 @@ var AuthPrompt = GObject.registerClass({ this.setActorInDefaultButtonWell(this._nextButton, animate); } - clear() { +- this._entryArea.hide(); +- this._entry.text = ''; +- this._inactiveEntry.text = ''; +- this.stopSpinning(); + clear(params) { + const {reuseEntryText} = Params.parse(params, { + reuseEntryText: false, + }); + + if (!reuseEntryText) { ++ this._entryArea.hide(); + this._entry.text = ''; + this._inactiveEntry.text = ''; ++ this.stopSpinning(); + } + - this._entryArea.hide(); -- this._entry.text = ''; -- this._inactiveEntry.text = ''; - this.stopSpinning(); this._authList.clear(); this._authList.hide(); -@@ -708,8 +714,13 @@ var AuthPrompt = GObject.registerClass({ + +@@ -706,8 +712,13 @@ var AuthPrompt = GObject.registerClass({ this.updateSensitivity(false); } @@ -1245,7 +1367,7 @@ index a970f17ca9..80c65eef90 100644 this.verificationStatus = AuthPromptStatus.NOT_VERIFYING; this.cancelButton.reactive = this._hasCancelButton; this.cancelButton.can_focus = this._hasCancelButton; -@@ -725,7 +736,7 @@ var AuthPrompt = GObject.registerClass({ +@@ -723,7 +734,7 @@ var AuthPrompt = GObject.registerClass({ this._userVerifier.cancel(); this._queryingService = null; @@ -1254,7 +1376,7 @@ index a970f17ca9..80c65eef90 100644 this._message.opacity = 0; this.setUser(null); this._updateEntry(true); -@@ -750,7 +761,8 @@ var AuthPrompt = GObject.registerClass({ +@@ -748,7 +759,8 @@ var 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; @@ -1265,37 +1387,176 @@ index a970f17ca9..80c65eef90 100644 beginRequestType = BeginRequestType.REUSE_USERNAME; } else { -- -2.51.0 +2.55.0 -From 17de39832ff7a953840f898a8bac829ab6f560e6 Mon Sep 17 00:00:00 2001 +From f840dbf33404595575a39eabbad1d4fe62c326c9 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Tue, 19 May 2026 15:18:23 +0200 +Subject: [PATCH 22/73] 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 fa7fb2a43..512d1ec30 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -738,7 +738,6 @@ var AuthPrompt = GObject.registerClass({ + this._message.opacity = 0; + this.setUser(null); + this._updateEntry(true); +- this.stopSpinning(); + + if (oldStatus == AuthPromptStatus.VERIFICATION_FAILED) + this.emit('failed'); +-- +2.55.0 + + +From fc52d55220874c41417111f80427630852024814 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Tue, 21 Apr 2026 13:06:57 +0200 +Subject: [PATCH 23/73] 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 512d1ec30..d69666d85 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -555,6 +555,8 @@ var AuthPrompt = GObject.registerClass({ + }); + + if (!reuseEntryText) { ++ this._entry.hint_text = ''; ++ this._inactiveEntry.hint_text = ''; + this._entryArea.hide(); + this._entry.text = ''; + this._inactiveEntry.text = ''; +-- +2.55.0 + + +From a6fbe9983921c8c24d2e5843413e0360befd9713 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Fri, 3 Jul 2026 13:06:26 +0200 +Subject: [PATCH 24/73] 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 d69666d85..d9da6f63f 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -337,7 +337,7 @@ var AuthPrompt = GObject.registerClass({ + this.emit('next'); + } + +- _updateEntry(secret) { ++ updateEntry(secret) { + let newEntry, inactiveEntry; + + if (secret && this._entry !== this._passwordEntry) { +@@ -371,7 +371,7 @@ var 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 +@@ -696,9 +696,6 @@ var AuthPrompt = GObject.registerClass({ + + let userWidget = new UserWidget.UserWidget(user, Clutter.Orientation.VERTICAL); + this._userWell.set_child(userWidget); +- +- if (!user) +- this._updateEntry(false); + } + + _onUserStoppedTypePreemptiveAnswer() { +@@ -739,7 +736,7 @@ var 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 e8103fec8..d188a7e85 100644 +--- a/js/gdm/loginDialog.js ++++ b/js/gdm/loginDialog.js +@@ -947,6 +947,7 @@ var 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 5913647674143a1e7b0c5119cafaa2dc3414f25d Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Thu, 12 Feb 2026 16:56:48 +0100 -Subject: [PATCH 20/48] authPrompt: Rename BeginRequestType to ResetType +Subject: [PATCH 25/73] 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 | 15 ++++++++------- + js/gdm/authPrompt.js | 14 +++++++------- js/gdm/loginDialog.js | 6 +++--- js/ui/unlockDialog.js | 4 ++-- - 3 files changed, 13 insertions(+), 12 deletions(-) + 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index 80c65eef90..15e82fc212 100644 +index d9da6f63f..13fd8310a 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -35,7 +35,8 @@ var AuthPromptStatus = { +@@ -35,7 +35,7 @@ var AuthPromptStatus = { VERIFICATION_IN_PROGRESS: 5, }; -var BeginRequestType = { -+/** @enum {number} */ +var ResetType = { PROVIDE_USERNAME: 0, DONT_PROVIDE_USERNAME: 1, REUSE_USERNAME: 2, -@@ -747,30 +748,30 @@ var AuthPrompt = GObject.registerClass({ +@@ -743,30 +743,30 @@ var AuthPrompt = GObject.registerClass({ else if (oldStatus === AuthPromptStatus.VERIFICATION_CANCELLED) this.emit('cancelled'); @@ -1333,7 +1594,7 @@ index 80c65eef90..15e82fc212 100644 addCharacter(unichar) { diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js -index bc62416e88..3f3d8702cb 100644 +index d188a7e85..6355f5f8b 100644 --- a/js/gdm/loginDialog.js +++ b/js/gdm/loginDialog.js @@ -865,7 +865,7 @@ var LoginDialog = GObject.registerClass({ @@ -1360,10 +1621,10 @@ index bc62416e88..3f3d8702cb 100644 this._showUserList(); else diff --git a/js/ui/unlockDialog.js b/js/ui/unlockDialog.js -index 19df7fc7b2..ba32d45d5d 100644 +index 6e69cd079..0d0b779e1 100644 --- a/js/ui/unlockDialog.js +++ b/js/ui/unlockDialog.js -@@ -800,9 +800,9 @@ var UnlockDialog = GObject.registerClass({ +@@ -788,9 +788,9 @@ var UnlockDialog = GObject.registerClass({ this.emit('failed'); } @@ -1376,27 +1637,29 @@ index 19df7fc7b2..ba32d45d5d 100644 userName = this._userName; } else { -- -2.51.0 +2.55.0 -From 7780db5e45adadb74998adf5937badd9c601a4b2 Mon Sep 17 00:00:00 2001 +From 7101ed74b632650a59bf902858be26923cee0144 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Wed, 11 Feb 2026 16:11:59 +0100 -Subject: [PATCH 21/48] unlockDialog: Use isprint() instead of isgraph() for +Subject: [PATCH 26/73] 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(-) diff --git a/js/ui/unlockDialog.js b/js/ui/unlockDialog.js -index ba32d45d5d..941bcf7b51 100644 +index 0d0b779e1..6053a9ab5 100644 --- a/js/ui/unlockDialog.js +++ b/js/ui/unlockDialog.js -@@ -647,7 +647,7 @@ var UnlockDialog = GObject.registerClass({ +@@ -635,7 +635,7 @@ var UnlockDialog = GObject.registerClass({ this._showPrompt(); @@ -1406,90 +1669,85 @@ index ba32d45d5d..941bcf7b51 100644 return Clutter.EVENT_PROPAGATE; -- -2.51.0 +2.55.0 -From 47dfb8390cb132ece1832b17754aaf56808111e5 Mon Sep 17 00:00:00 2001 +From c6631307e1cea4995fbf7416bf38d03b9ec5aeb3 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Wed, 21 Jan 2026 14:34:11 +0100 -Subject: [PATCH 22/48] authPrompt: Capture preemptive input before entry is +Subject: [PATCH 27/73] authPrompt: Capture preemptive input before entry is sensitive The previous changes made the entry invisible and insensitive until askQuestion is called. During the lock screen, when the user starts typing before the PAM service is ready, keystrokes were being lost. -Replace addCharacter() with startPreemptiveInput() which buffers -keystrokes and handles Enter key presses while waiting for the entry -to become sensitive. +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 | 43 +++++++++++++++++++++++++++++++++++-------- + js/gdm/authPrompt.js | 37 +++++++++++++++++++++++++------------ js/gdm/loginDialog.js | 4 ---- js/ui/unlockDialog.js | 2 +- - 3 files changed, 36 insertions(+), 13 deletions(-) + 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index 15e82fc212..8f5751ec99 100644 +index 13fd8310a..8f65d81bd 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -143,6 +143,10 @@ var AuthPrompt = GObject.registerClass({ - vfunc_key_press_event(keyPressEvent) { - if (keyPressEvent.keyval == Clutter.KEY_Escape) - this.cancel(); -+ -+ if (this._preemptiveInput && !this._pendingActivate) -+ return this._entry.clutter_text.event(keyPressEvent, false); -+ - return super.vfunc_key_press_event(keyPressEvent); - } +@@ -334,6 +334,8 @@ var AuthPrompt = GObject.registerClass({ + } + } -@@ -242,7 +246,16 @@ var AuthPrompt = GObject.registerClass({ - this._fadeOutMessage(); - }); - -- entry.clutter_text.connect('activate', () => this._activateNext()); -+ entry.clutter_text.connect('activate', () => { -+ if (this._preemptiveInput && -+ !this._pendingActivate && -+ this._entry.clutter_text.text) { -+ this._pendingActivate = true; -+ return; -+ } -+ -+ this._activateNext(); -+ }); - }); - - this._defaultButtonWell = new St.Widget({ -@@ -581,10 +594,24 @@ var AuthPrompt = GObject.registerClass({ - 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._completePreemptiveInput({canActivate: element === this._entryArea}); -+ }, -+ onStopped: (isFinished) => { -+ if (!isFinished) -+ this._completePreemptiveInput(); -+ }, - }); - } - -+ _completePreemptiveInput({canActivate = false} = {}) { -+ if (this._pendingActivate && canActivate) -+ this._activateNext(); + this._preemptiveInput = false; -+ this._pendingActivate = false; -+ } + - setChoiceList(promptMessage, choiceList) { - this._authList.clear(); - this._authList.label.text = promptMessage; -@@ -716,7 +743,7 @@ var AuthPrompt = GObject.registerClass({ + this.emit('next'); + } + +@@ -365,9 +367,11 @@ var 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; + } + +@@ -390,8 +394,7 @@ var AuthPrompt = GObject.registerClass({ + + this._queryingService = serviceName; + +- if (this._preemptiveAnswer) +- this._preemptiveAnswer = null; ++ this._clearPreemptiveState(); + + this.setChoiceList(promptMessage, choiceList); + this.updateSensitivity({sensitive: true}); +@@ -659,6 +662,9 @@ var AuthPrompt = GObject.registerClass({ + } + + updateSensitivity({sensitive}) { ++ if (sensitive && this._preemptiveAnswer) ++ return; ++ + const authWidget = [ + this._authList, + ].find(widget => widget.visible) ?? this._entry; +@@ -712,7 +718,7 @@ var AuthPrompt = GObject.registerClass({ } reset(params) { @@ -1498,7 +1756,7 @@ index 15e82fc212..8f5751ec99 100644 reuseEntryText: false, softReset: false, }); -@@ -736,6 +763,8 @@ var AuthPrompt = GObject.registerClass({ +@@ -732,6 +738,8 @@ var AuthPrompt = GObject.registerClass({ if (this._userVerifier) this._userVerifier.cancel(); @@ -1507,27 +1765,42 @@ index 15e82fc212..8f5751ec99 100644 this._queryingService = null; this.clear({reuseEntryText}); this._message.opacity = 0; -@@ -774,12 +803,10 @@ var AuthPrompt = GObject.registerClass({ +@@ -769,19 +777,24 @@ var 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._entry.clutter_text.insert_unichar(unichar); -+ this.grab_key_focus(); ++ this.updateSensitivity({sensitive: true}); ++ 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) { + params = Params.parse(params, { userName: null, + 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 3f3d8702cb..36a727f01f 100644 +index 6355f5f8b..0d5c88d99 100644 --- a/js/gdm/loginDialog.js +++ b/js/gdm/loginDialog.js -@@ -1329,10 +1329,6 @@ var LoginDialog = GObject.registerClass({ +@@ -1328,10 +1328,6 @@ var LoginDialog = GObject.registerClass({ this._authPrompt.cancel(); } @@ -1539,10 +1812,10 @@ index 3f3d8702cb..36a727f01f 100644 this._authPrompt.finish(onComplete); } diff --git a/js/ui/unlockDialog.js b/js/ui/unlockDialog.js -index 941bcf7b51..07b2acaf45 100644 +index 6053a9ab5..6ce8e2f41 100644 --- a/js/ui/unlockDialog.js +++ b/js/ui/unlockDialog.js -@@ -648,7 +648,7 @@ var UnlockDialog = GObject.registerClass({ +@@ -636,7 +636,7 @@ var UnlockDialog = GObject.registerClass({ this._showPrompt(); if (GLib.unichar_isprint(unichar)) @@ -1552,46 +1825,60 @@ index 941bcf7b51..07b2acaf45 100644 return Clutter.EVENT_PROPAGATE; } -- -2.51.0 +2.55.0 -From d04618600781d432113ce68b4fed56fbb6f06878 Mon Sep 17 00:00:00 2001 +From abd59a87a856481604143c66de4752fc045e3341 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Tue, 10 Feb 2026 18:51:41 +0100 -Subject: [PATCH 23/48] authPrompt: On verificationFailed ensure input - sensitivity is disabled +Subject: [PATCH 28/73] authPrompt: On verificationFailed allow a + preemptiveInput There's a time window between the verification failing and a new verification request being started. -Input is not accepted during this time, so disable input sensitivity -when verification fails. UserVerifier will be in charge of restarting the -verification process, reenabling sensitivity. +During this time, allow input sensitivity so a new answer can be +inserted before being requested. + +Part-of: --- - js/gdm/authPrompt.js | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) + 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 8f5751ec99..48f2c8cdd6 100644 +index 8f65d81bd..44cffe786 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -455,7 +455,7 @@ var AuthPrompt = GObject.registerClass({ +@@ -460,7 +460,11 @@ var AuthPrompt = GObject.registerClass({ if (wasQueryingService) this._queryingService = null; - this.updateSensitivity({sensitive: canRetry}); -+ this.updateSensitivity({sensitive: false}); ++ if (canRetry) { ++ this.verificationStatus = AuthPromptStatus.VERIFYING; ++ this._entry.text = ''; ++ this.startPreemptiveInput(); ++ } this.stopSpinning(); if (!canRetry) +@@ -738,7 +742,7 @@ var AuthPrompt = GObject.registerClass({ + if (this._userVerifier) + this._userVerifier.cancel(); + +- reuseEntryText = reuseEntryText || this._preemptiveInput; ++ reuseEntryText = reuseEntryText || !!this._preemptiveAnswer || this._preemptiveInput; + + this._queryingService = null; + this.clear({reuseEntryText}); -- -2.51.0 +2.55.0 -From e20104b165e40b34282b1559a046061d55327513 Mon Sep 17 00:00:00 2001 +From ddaba5bead9ad5d742d6fb53e07a9eea5da89343 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Tue, 30 Sep 2025 17:43:22 +0200 -Subject: [PATCH 24/48] authPrompt: Update authList style +Subject: [PATCH 29/73] authPrompt: Update authList style Make the AuthListItem buttons a bit bigger and more rounded. @@ -1605,25 +1892,27 @@ back button is visible and properly aligned. Use accessible_name. Register classes with the new style. + +Part-of: --- - js/gdm/authList.js | 169 +++++++++++++++++++++++++++++++++++-------- - js/gdm/authPrompt.js | 30 ++++++-- + js/gdm/authList.js | 188 +++++++++++++++++++++++++++++++++++-------- + js/gdm/authPrompt.js | 42 +++++++--- js/gdm/util.js | 6 +- - 3 files changed, 167 insertions(+), 38 deletions(-) + 3 files changed, 194 insertions(+), 42 deletions(-) diff --git a/js/gdm/authList.js b/js/gdm/authList.js -index fb223a9727..3f1b2817f9 100644 +index fb223a972..ac2165e46 100644 --- a/js/gdm/authList.js +++ b/js/gdm/authList.js -@@ -17,29 +17,134 @@ +@@ -17,30 +17,140 @@ */ /* exported AuthList */ -const { Clutter, GObject, Meta, St } = imports.gi; -+const { Clutter, Graphene, Shell, GObject, Meta, St } = imports.gi; ++const { Clutter, Graphene, GObject, Meta, Pango, Shell, St } = imports.gi; + -+const Main = imports.ui.main; -+const PopupMenu = imports.ui.popupMenu; ++import * as Main from '../ui/main.js'; ++import * as PopupMenu from '../ui/popupMenu.js'; const SCROLL_ANIMATION_TIME = 500; @@ -1637,7 +1926,7 @@ index fb223a9727..3f1b2817f9 100644 - style_class: 'login-dialog-auth-list-label', - y_align: Clutter.ActorAlign.CENTER, - x_expand: false, -+const ItemIconPopup = class extends PopupMenu.PopupMenu { ++var ItemIconPopup = class extends PopupMenu.PopupMenu { + constructor(sourceActor, title, subtitle) { + super(sourceActor, 0.5, St.Side.TOP); + @@ -1658,8 +1947,8 @@ index fb223a9727..3f1b2817f9 100644 + 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(); + @@ -1669,8 +1958,9 @@ index fb223a9727..3f1b2817f9 100644 + this._menuManager.addMenu(this); + } +}; -+ -+class ItemIcon extends St.Button { + +- super._init({ ++var ItemIcon = class extends St.Button { + static { + GObject.registerClass(this); + } @@ -1678,6 +1968,7 @@ index fb223a9727..3f1b2817f9 100644 + constructor(iconName, iconTitle, iconSubtitle) { + super({ + style_class: 'login-dialog-item-icon', ++ button_mask: St.ButtonMask.PRIMARY | St.ButtonMask.SECONDARY, + iconName, + }); + @@ -1694,8 +1985,7 @@ index fb223a9727..3f1b2817f9 100644 + static { + GObject.registerClass(this); + } - -- super._init({ ++ + constructor(key, content) { + const {title, subtitle, iconName, iconTitle, iconSubtitle} = content; + @@ -1708,14 +1998,14 @@ index fb223a9727..3f1b2817f9 100644 + accessible_name: [title, subtitle, iconTitle, iconSubtitle] + .filter(p => p) + .join(', '), -+ }); -+ + }); + + this.key = key; + + this._container = new St.Widget({ + layout_manager: new Clutter.BinLayout(), + x_expand: true, - }); ++ }); + this._labelBox = new St.BoxLayout({ + orientation: Clutter.Orientation.VERTICAL, + y_align: Clutter.ActorAlign.CENTER, @@ -1730,6 +2020,7 @@ index fb223a9727..3f1b2817f9 100644 + y_align: Clutter.ActorAlign.CENTER, + x_expand: true, + }); ++ label.clutter_text.ellipsize = Pango.EllipsizeMode.END; + this._labelBox.add_child(label); + } + @@ -1740,6 +2031,7 @@ index fb223a9727..3f1b2817f9 100644 + y_align: Clutter.ActorAlign.CENTER, + x_expand: true, + }); ++ label.clutter_text.ellipsize = Pango.EllipsizeMode.END; + this._labelBox.add_child(label); + } + @@ -1759,11 +2051,14 @@ index fb223a9727..3f1b2817f9 100644 + this._container.add_child(icon); + } + ++ this.compact = !subtitle && !iconName; ++ + this.set_child(this._container); - ++ this.connect('key-focus-in', () => this._setSelected(true)); -@@ -63,25 +168,26 @@ const AuthListItem = GObject.registerClass({ + this.connect('key-focus-out', +@@ -63,25 +173,26 @@ const AuthListItem = GObject.registerClass({ this.remove_style_pseudo_class('selected'); } } @@ -1779,7 +2074,7 @@ index fb223a9727..3f1b2817f9 100644 - _init() { - super._init({ - vertical: true, -+export class AuthList extends St.BoxLayout { ++var AuthList = class extends St.BoxLayout { + static [GObject.signals] = { + 'activate': {param_types: [GObject.TYPE_STRING]}, + 'item-added': {param_types: [AuthListItem.$gtype]}, @@ -1804,11 +2099,32 @@ index fb223a9727..3f1b2817f9 100644 this._scrollView = new St.ScrollView({ style_class: 'login-dialog-auth-list-view', }); -@@ -135,11 +241,11 @@ var AuthList = GObject.registerClass({ +@@ -98,6 +209,10 @@ var AuthList = GObject.registerClass({ + this._scrollView.add_actor(this._box); + 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)); + } + +@@ -135,11 +250,21 @@ var 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); @@ -1819,7 +2135,7 @@ index fb223a9727..3f1b2817f9 100644 this._items.set(key, item); -@@ -169,8 +275,7 @@ var AuthList = GObject.registerClass({ +@@ -169,8 +294,7 @@ var AuthList = GObject.registerClass({ } clear() { @@ -1830,11 +2146,23 @@ index fb223a9727..3f1b2817f9 100644 -}); +} diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index 48f2c8cdd6..84c4575518 100644 +index 44cffe786..8174abf8d 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -193,13 +193,32 @@ var AuthPrompt = GObject.registerClass({ - duration: MESSAGE_FADE_OUT_ANIMATION_TIME, +@@ -169,23 +169,46 @@ var AuthPrompt = GObject.registerClass({ + this._mainBox.add_child(this.cancelButton); + + this._authList = new AuthList.AuthList(); +- this._authList.set({ +- visible: false, +- }); ++ this._authList.hide(); + this._authList.connect('activate', (list, key) => { + this._authList.reactive = false; + this._authList.ease({ + 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 = ''; @@ -1845,37 +2173,43 @@ index 48f2c8cdd6..84c4575518 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', + this._authListTitle, 'visible', -+ GObject.BindingFlags.SYNC_CREATE); ++ GObject.BindingFlags.DEFAULT); + this._authList.bind_property('opacity', + this._authListTitle, 'opacity', -+ GObject.BindingFlags.SYNC_CREATE); ++ GObject.BindingFlags.DEFAULT); + this._mainBox.add_child(this._authListTitle); ++ ++ 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', -@@ -560,6 +579,7 @@ var AuthPrompt = GObject.registerClass({ +@@ -570,6 +593,7 @@ var AuthPrompt = GObject.registerClass({ + this.stopSpinning(); + } - this._entryArea.hide(); - this.stopSpinning(); + this._authListTitle.child.text = ''; this._authList.clear(); this._authList.hide(); -@@ -614,10 +634,10 @@ var AuthPrompt = GObject.registerClass({ +@@ -608,10 +632,10 @@ var AuthPrompt = GObject.registerClass({ setChoiceList(promptMessage, choiceList) { this._authList.clear(); @@ -1891,7 +2225,7 @@ index 48f2c8cdd6..84c4575518 100644 this._entryArea.hide(); diff --git a/js/gdm/util.js b/js/gdm/util.js -index 38a7015de8..b6327c5729 100644 +index 38a7015de..6a4c93fde 100644 --- a/js/gdm/util.js +++ b/js/gdm/util.js @@ -762,7 +762,11 @@ var ShellUserVerifier = class { @@ -1900,7 +2234,7 @@ index 38a7015de8..b6327c5729 100644 - this.emit('show-choice-list', serviceName, promptMessage, list.deep_unpack()); + const choiceList = {}; -+ for (const [key, value] of Object.entries(list.deepUnpack())) ++ for (const [key, value] of Object.entries(list.deep_unpack())) + choiceList[key] = {title: value}; + + this.emit('show-choice-list', serviceName, promptMessage, choiceList); @@ -1908,13 +2242,13 @@ index 38a7015de8..b6327c5729 100644 _onInfo(client, serviceName, info) { -- -2.51.0 +2.55.0 -From f9b334f264d5d04b9bb460a28b245a72ddd3fc24 Mon Sep 17 00:00:00 2001 +From 8660682e4f45dfae3d9477d0871bd106e3e30b1d Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Wed, 8 Oct 2025 18:48:07 +0200 -Subject: [PATCH 25/48] authPrompt: Let back button go back to step 1 instead +Subject: [PATCH 30/73] 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 @@ -1923,15 +2257,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 | 38 ++++++++++++++++++++++++++++---------- - 1 file changed, 28 insertions(+), 10 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 84c4575518..ad7b890c7c 100644 +index 8174abf8d..810d76c13 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -67,6 +67,7 @@ var AuthPrompt = GObject.registerClass({ +@@ -66,6 +66,7 @@ var AuthPrompt = GObject.registerClass({ this._mode = mode; this._defaultButtonWellActor = null; this._cancelledRetries = 0; @@ -1939,7 +2275,7 @@ index 84c4575518..ad7b890c7c 100644 this._idleMonitor = Meta.IdleMonitor.get_core(); -@@ -98,8 +99,6 @@ var AuthPrompt = GObject.registerClass({ +@@ -97,8 +98,6 @@ var AuthPrompt = GObject.registerClass({ }); this.add_child(this._userWell); @@ -1948,21 +2284,21 @@ index 84c4575518..ad7b890c7c 100644 this._initInputRow(); let capsLockPlaceholder = new St.Label(); -@@ -163,8 +162,8 @@ var AuthPrompt = GObject.registerClass({ +@@ -154,18 +153,16 @@ var AuthPrompt = GObject.registerClass({ + + this.cancelButton = new St.Button({ style_class: 'modal-dialog-button 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, + reactive: true, + can_focus: true, - x_expand: true, x_align: Clutter.ActorAlign.START, y_align: Clutter.ActorAlign.CENTER, -@@ -176,10 +175,8 @@ var AuthPrompt = GObject.registerClass({ - pivot_point: new Graphene.Point({x: 1, y: 0}), - })); - + child: new St.Icon({ icon_name: 'go-previous-symbolic' }), + }); - if (this._hasCancelButton) - this.cancelButton.connect('clicked', () => this.cancel()); - else @@ -1972,7 +2308,7 @@ index 84c4575518..ad7b890c7c 100644 this._mainBox.add_child(this.cancelButton); this._authList = new AuthList.AuthList(); -@@ -310,6 +307,16 @@ var AuthPrompt = GObject.registerClass({ +@@ -302,6 +299,15 @@ var AuthPrompt = GObject.registerClass({ } catch (e) {} } @@ -1981,34 +2317,32 @@ index 84c4575518..ad7b890c7c 100644 + return; + + const cancelVisible = this._promptStep > 1; -+ this.cancelButton.visible = cancelVisible; ++ this.cancelButton.opacity = cancelVisible ? 255 : 0; + this.cancelButton.reactive = cancelVisible; -+ this.cancelButton.can_focus = cancelVisible; + } + showTimedLoginIndicator(time) { let hold = new Batch.Hold(); -@@ -396,6 +403,9 @@ var 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; -@@ -420,6 +430,8 @@ var AuthPrompt = GObject.registerClass({ +@@ -390,6 +396,8 @@ var AuthPrompt = GObject.registerClass({ this.clear(); this._queryingService = serviceName; + this._promptStep++; + this._updateCancelButton(); - if (this._preemptiveAnswer) - this._preemptiveAnswer = null; -@@ -770,10 +782,10 @@ var AuthPrompt = GObject.registerClass({ + const preemptiveAnswer = this._preemptiveAnswer; + this._clearPreemptiveState(); +@@ -416,6 +424,8 @@ var AuthPrompt = GObject.registerClass({ + this.clear(); + + this._queryingService = serviceName; ++ this._promptStep++; ++ this._updateCancelButton(); + + this._clearPreemptiveState(); + +@@ -753,10 +763,10 @@ var AuthPrompt = GObject.registerClass({ const oldStatus = this.verificationStatus; this.verificationStatus = AuthPromptStatus.NOT_VERIFYING; @@ -2021,7 +2355,7 @@ index 84c4575518..ad7b890c7c 100644 if (this._preemptiveAnswerWatchId) this._idleMonitor.remove_watch(this._preemptiveAnswerWatchId); -@@ -861,6 +873,12 @@ var AuthPrompt = GObject.registerClass({ +@@ -850,6 +860,12 @@ var AuthPrompt = GObject.registerClass({ if (this.verificationStatus == AuthPromptStatus.VERIFICATION_SUCCEEDED) return; @@ -2035,31 +2369,33 @@ index 84c4575518..ad7b890c7c 100644 this._cancelledRetries++; if (this._cancelledRetries > this._userVerifier.allowedFailures) -- -2.51.0 +2.55.0 -From 54e917b7974c0f1cdc12a5c00ff903912408b721 Mon Sep 17 00:00:00 2001 +From 4b713dfe5746d4e6eb8c7d5cc83fa3677479de8c Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Mon, 20 Oct 2025 17:24:40 +0200 -Subject: [PATCH 26/48] loginDialog: Vertically center authPrompt using fixed +Subject: [PATCH 31/73] 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 36a727f01f..8b0402ffdc 100644 +index 0d5c88d99..24f3e0a4f 100644 --- a/js/gdm/loginDialog.js +++ b/js/gdm/loginDialog.js @@ -34,6 +34,7 @@ const UserWidget = imports.ui.userWidget; 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; var UserListItem = GObject.registerClass({ @@ -2102,19 +2438,21 @@ index 36a727f01f..8b0402ffdc 100644 } -- -2.51.0 +2.55.0 -From 440192763a323db9e1b23d2d0e467d8c004f075f Mon Sep 17 00:00:00 2001 +From 0cc75a34d9d08f0971a4fc8e8b464ae9fdf1bc9c Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Tue, 12 Nov 2024 14:26:30 -0500 -Subject: [PATCH 27/48] data: Add fingerprint and vcard icons +Subject: [PATCH 32/73] 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: --- .../status/fingerprint-auth-symbolic.svg | 28 +++++++++++++++++++ data/icons/scalable/status/vcard-symbolic.svg | 9 ++++++ @@ -2124,7 +2462,7 @@ have an Organization field in its subject. diff --git a/data/icons/scalable/status/fingerprint-auth-symbolic.svg b/data/icons/scalable/status/fingerprint-auth-symbolic.svg new file mode 100644 -index 0000000000..67cce39bc1 +index 000000000..67cce39bc --- /dev/null +++ b/data/icons/scalable/status/fingerprint-auth-symbolic.svg @@ -0,0 +1,28 @@ @@ -2158,7 +2496,7 @@ index 0000000000..67cce39bc1 + style="fill:#ffffff;fill-opacity:1" /> diff --git a/data/icons/scalable/status/vcard-symbolic.svg b/data/icons/scalable/status/vcard-symbolic.svg new file mode 100644 -index 0000000000..1694f23645 +index 000000000..1694f2364 --- /dev/null +++ b/data/icons/scalable/status/vcard-symbolic.svg @@ -0,0 +1,9 @@ @@ -2172,13 +2510,13 @@ index 0000000000..1694f23645 + + -- -2.51.0 +2.55.0 -From 833e3979f1823836188043420f6cc44885c3c02f Mon Sep 17 00:00:00 2001 +From 793a0c11b99e6993f06b069d84f3240a97d01411 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Mon, 18 Aug 2025 12:30:50 +0200 -Subject: [PATCH 28/48] gdm: Extract authentication service and role constants +Subject: [PATCH 33/73] gdm: Extract authentication service and role constants to constants.js Create a dedicated const.js module to centralize GDM authentication-related @@ -2186,19 +2524,21 @@ 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/util.js | 32 +++++++++++++++----------------- + js/gdm/constants.js | 8 +++++++ + js/gdm/util.js | 45 ++++++++++++++++++++--------------- js/js-resources.gresource.xml | 1 + - 4 files changed, 28 insertions(+), 18 deletions(-) + 4 files changed, 37 insertions(+), 20 deletions(-) create mode 100644 js/gdm/constants.js diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index ad7b890c7c..0aca776324 100644 +index 810d76c13..ed7a5f084 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -6,6 +6,7 @@ const { Clutter, Gio, GLib, GObject, Graphene, Meta, Pango, Shell, St } = import +@@ -6,6 +6,7 @@ const { Clutter, Gio, GLib, GObject, Meta, Pango, Shell, St } = imports.gi; const Animation = imports.ui.animation; const AuthList = imports.gdm.authList; const Batch = imports.gdm.batch; @@ -2206,7 +2546,7 @@ index ad7b890c7c..0aca776324 100644 const GdmUtil = imports.gdm.util; const OVirt = imports.gdm.oVirt; const Vmware = imports.gdm.vmware; -@@ -456,7 +457,7 @@ var AuthPrompt = GObject.registerClass({ +@@ -449,7 +450,7 @@ var AuthPrompt = GObject.registerClass({ // with a smartcard // 2) Don't reset if we've already succeeded at verification and // the user is getting logged in. @@ -2217,12 +2557,10 @@ index ad7b890c7c..0aca776324 100644 this.smartcardDetected) diff --git a/js/gdm/constants.js b/js/gdm/constants.js new file mode 100644 -index 0000000000..2f37446c8a +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'; @@ -2232,7 +2570,7 @@ index 0000000000..2f37446c8a +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 b6327c5729..4a8bf09d18 100644 +index 6a4c93fde..013bb6b8b 100644 --- a/js/gdm/util.js +++ b/js/gdm/util.js @@ -6,6 +6,7 @@ const { Clutter, Gdm, Gio, GLib } = imports.gi; @@ -2273,7 +2611,25 @@ index b6327c5729..4a8bf09d18 100644 this._preemptingService = null; this._updateDefaultService(); -@@ -654,7 +652,7 @@ var ShellUserVerifier = class { +@@ -645,7 +643,16 @@ var ShellUserVerifier = class { + } + + serviceIsForeground(serviceName) { +- return serviceName == this._getForegroundService(); ++ return serviceName === this._getForegroundService(); ++ } ++ ++ foregroundServiceDeterminesUsername() { ++ for (let serviceName in this._credentialManagers) { ++ if (this.serviceIsForeground(serviceName)) ++ return true; ++ } ++ ++ return this.serviceIsForeground(Constants.SMARTCARD_SERVICE_NAME); + } + + serviceIsDefault(serviceName) { +@@ -654,7 +661,7 @@ var ShellUserVerifier = class { serviceIsFingerprint(serviceName) { return this._fingerprintReaderType !== FingerprintReaderType.NONE && @@ -2282,7 +2638,7 @@ index b6327c5729..4a8bf09d18 100644 } _onSettingsChanged() { -@@ -671,7 +669,7 @@ var ShellUserVerifier = class { +@@ -671,7 +678,7 @@ var ShellUserVerifier = class { this._fingerprintManager = null; this._fingerprintReaderType = FingerprintReaderType.NONE; @@ -2291,7 +2647,7 @@ index b6327c5729..4a8bf09d18 100644 needsReset = true; } -@@ -682,7 +680,7 @@ var ShellUserVerifier = class { +@@ -682,7 +689,7 @@ var ShellUserVerifier = class { this._smartcardManager.disconnect(this._smartcardRemovedId); this._smartcardManager = null; @@ -2300,7 +2656,7 @@ index b6327c5729..4a8bf09d18 100644 needsReset = true; } -@@ -692,13 +690,13 @@ var ShellUserVerifier = class { +@@ -692,13 +699,13 @@ var ShellUserVerifier = class { _getDetectedDefaultService() { if (this._smartcardManager?.loggedInWithToken()) @@ -2318,16 +2674,18 @@ index b6327c5729..4a8bf09d18 100644 return null; } -@@ -708,7 +706,7 @@ var ShellUserVerifier = class { +@@ -707,8 +714,8 @@ var ShellUserVerifier = class { + this._defaultService = this._getDetectedDefaultService(); if (!this._defaultService) { - log("no authentication service is enabled, using password authentication"); +- log("no authentication service is enabled, using password authentication"); - this._defaultService = PASSWORD_SERVICE_NAME; ++ log('no authentication service is enabled, using password authentication'); + this._defaultService = Constants.PASSWORD_SERVICE_NAME; } if (oldDefaultService && -@@ -754,8 +752,8 @@ var ShellUserVerifier = class { +@@ -754,8 +761,8 @@ var ShellUserVerifier = class { async _maybeStartFingerprintVerification() { if (this._userName && this._fingerprintReaderType !== FingerprintReaderType.NONE && @@ -2338,7 +2696,7 @@ index b6327c5729..4a8bf09d18 100644 } _onChoiceListQuery(client, serviceName, promptMessage, list) { -@@ -880,7 +878,7 @@ var ShellUserVerifier = class { +@@ -880,7 +887,7 @@ var ShellUserVerifier = class { } _verificationFailed(serviceName, shouldRetry) { @@ -2348,7 +2706,7 @@ index b6327c5729..4a8bf09d18 100644 GLib.source_remove(this._fingerprintFailedId); } diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml -index b2c603a55a..ec6dee70b2 100644 +index b2c603a55..ec6dee70b 100644 --- a/js/js-resources.gresource.xml +++ b/js/js-resources.gresource.xml @@ -4,6 +4,7 @@ @@ -2360,13 +2718,13 @@ index b2c603a55a..ec6dee70b2 100644 gdm/oVirt.js gdm/credentialManager.js -- -2.51.0 +2.55.0 -From 7763c1b3b91810be91ce2be4d8df05b2aabb38e4 Mon Sep 17 00:00:00 2001 +From 5e5b4b93e0eeea070d5505d3925a46cab9971d6c Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Wed, 11 Mar 2026 11:41:40 +0100 -Subject: [PATCH 29/48] gdm/util: Add helper functions to get future mechanisms +Subject: [PATCH 34/73] gdm/util: Add helper functions to get future mechanisms metadata 1. isSelectable() to detect when a mechanism is selectable or runs in @@ -2374,15 +2732,26 @@ Subject: [PATCH 29/48] 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(+) + js/gdm/util.js | 37 ++++++++++++++++++++++++++++++++++++- + 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/js/gdm/util.js b/js/gdm/util.js -index 4a8bf09d18..691442ad90 100644 +index 013bb6b8b..2a5bb4804 100644 --- a/js/gdm/util.js +++ b/js/gdm/util.js -@@ -137,6 +137,40 @@ function cloneAndFadeOutActor(actor) { +@@ -1,6 +1,7 @@ + // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + /* exported BANNER_MESSAGE_KEY, BANNER_MESSAGE_TEXT_KEY, LOGO_KEY, +- DISABLE_USER_LIST_KEY, fadeInActor, fadeOutActor, cloneAndFadeOutActor */ ++ DISABLE_USER_LIST_KEY, fadeInActor, fadeOutActor, cloneAndFadeOutActor, ++ isSelectable, getNonSelectableIconName */ + + const { Clutter, Gdm, Gio, GLib } = imports.gi; + const Signals = imports.signals; +@@ -137,6 +138,40 @@ function cloneAndFadeOutActor(actor) { return hold; } @@ -2390,7 +2759,7 @@ index 4a8bf09d18..691442ad90 100644 + * @param {object} mechanism + * @returns {boolean} + */ -+export function isSelectable(mechanism) { ++function isSelectable(mechanism) { + switch (mechanism.role) { + case Constants.PASSWORD_ROLE_NAME: + case Constants.SMARTCARD_ROLE_NAME: @@ -2406,7 +2775,7 @@ index 4a8bf09d18..691442ad90 100644 + * @param {object} mechanism + * @returns {string} + */ -+export function getNonSelectableIconName(mechanism) { ++function getNonSelectableIconName(mechanism) { + // This is only used for non selectable mechanisms. + // Currently only fingerprint is non selectable + if (isSelectable(mechanism)) @@ -2424,13 +2793,13 @@ index 4a8bf09d18..691442ad90 100644 constructor(client, params) { params = Params.parse(params, { reauthenticationOnly: false }); -- -2.51.0 +2.55.0 -From a8303da3be45bec905d11e7e8a64ffa9088f6887 Mon Sep 17 00:00:00 2001 +From 7247d1a593dd6bae33d0e84b72c096bae3870b80 Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Tue, 6 Feb 2024 11:04:20 -0500 -Subject: [PATCH 30/48] gdm: Add new AuthMenuButton control +Subject: [PATCH 35/73] 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. @@ -2448,19 +2817,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: --- - js/gdm/authMenuButton.js | 532 ++++++++++++++++++++++++++++++++++ + js/gdm/authMenuButton.js | 514 ++++++++++++++++++++++++++++++++++ js/js-resources.gresource.xml | 1 + - 2 files changed, 533 insertions(+) + 2 files changed, 515 insertions(+) create mode 100644 js/gdm/authMenuButton.js diff --git a/js/gdm/authMenuButton.js b/js/gdm/authMenuButton.js new file mode 100644 -index 0000000000..9b7cebf6d8 +index 000000000..5aaa45fd3 --- /dev/null +++ b/js/gdm/authMenuButton.js -@@ -0,0 +1,532 @@ -+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- +@@ -0,0 +1,514 @@ +/* + * Copyright 2024 Red Hat, Inc + * @@ -2511,7 +2881,6 @@ index 0000000000..9b7cebf6d8 +import Shell from 'gi://Shell'; +import St from 'gi://St'; + -+import * as BoxPointer from '../ui/boxpointer.js'; +import * as Main from '../ui/main.js'; +import * as PopupMenu from '../ui/popupMenu.js'; + @@ -2579,21 +2948,11 @@ index 0000000000..9b7cebf6d8 + this.add_child(nameLabel); + this.label_actor = nameLabel; + } -+ -+ this.setOrnament(PopupMenu.Ornament.HIDDEN); + } +} + +export class AuthMenuButton extends St.Button { + static [GObject.properties] = { -+ 'title': GObject.ParamSpec.string( -+ 'title', null, null, -+ GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY, -+ ''), -+ 'icon-name': GObject.ParamSpec.string( -+ 'icon-name', null, null, -+ GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY, -+ ''), + 'read-only': GObject.ParamSpec.boolean( + 'read-only', null, null, + GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY, @@ -2603,7 +2962,7 @@ index 0000000000..9b7cebf6d8 + GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY), + 'animate-visibility': GObject.ParamSpec.boolean( + 'animate-visibility', null, null, -+ GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY, ++ GObject.ParamFlags.READWRITE, + false), + }; + @@ -2615,20 +2974,19 @@ index 0000000000..9b7cebf6d8 + GObject.registerClass(this); + } + -+ constructor(params) { -+ params.sectionOrder ??= []; ++ constructor(params = {}) { + super({ ++ icon_name: 'cog-wheel-symbolic', ++ sectionOrder: [], + ...params, + style_class: 'login-dialog-button login-dialog-auth-menu-button', -+ reactive: true, -+ track_hover: true, + can_focus: true, -+ accessible_name: params.title, + accessible_role: Atk.Role.MENU, -+ x_align: Clutter.ActorAlign.CENTER, -+ y_align: Clutter.ActorAlign.CENTER, ++ button_mask: St.ButtonMask.PRIMARY | St.ButtonMask.SECONDARY, + }); -+ this.child = this._createChild(); ++ this.bind_property('reactive', ++ this, 'can-focus', ++ GObject.BindingFlags.SYNC_CREATE); + + this._menu = new PopupMenu.PopupMenu(this, 0, St.Side.BOTTOM); + this._menu.box.add_style_class_name('login-dialog-auth-menu-button-popup'); @@ -2639,80 +2997,27 @@ index 0000000000..9b7cebf6d8 + {actionMode: Shell.ActionMode.NONE}); + this._manager.addMenu(this._menu); + -+ this.connect('clicked', () => { -+ if (this.readOnly && this._getVisibleItemsCount() === 1) -+ return; -+ this._menu.toggle(); -+ }); ++ this.connect('clicked', () => this._onClicked()); + + this._items = new Map(); + this._sections = new Map(); + this._activeItems = new Set(); -+ this.updateSensitivity(true); + } + -+ _createChild() { -+ return new St.Icon({icon_name: this.iconName}); -+ } -+ -+ _getMenuItem(item) { -+ if (!item) -+ return null; -+ -+ return this._items.get(JSON.stringify(item)); ++ _onClicked() { ++ this._menu.toggle(); + } + + _getVisibleItemsCount() { + return Array.from(this._items.values()).filter(item => item.visible).length; + } + -+ updateReactive(reactive) { -+ this.reactive = reactive; -+ this.can_focus = reactive; -+ } -+ -+ updateSensitivity(sensitive) { -+ this._sensitive = sensitive; -+ -+ const visibleItems = this._getVisibleItemsCount(); -+ if (visibleItems === 0 || (visibleItems <= 1 && !this.readOnly)) -+ sensitive = false; -+ -+ this.reactive = sensitive; -+ this.can_focus = sensitive; -+ this._menu.close(BoxPointer.PopupAnimation.NONE); -+ -+ if (this.animateVisibility) { -+ if (sensitive) { -+ this.opacity = 0; -+ this.visible = true; -+ this.ease({ -+ opacity: 255, -+ duration: VISIBILITY_ANIMATION_TIME, -+ mode: Clutter.AnimationMode.EASE_OUT_QUAD, -+ }); -+ } else { -+ this.ease({ -+ opacity: 0, -+ duration: VISIBILITY_ANIMATION_TIME, -+ mode: Clutter.AnimationMode.EASE_OUT_QUAD, -+ onComplete: () => { -+ this.visible = false; -+ }, -+ }); -+ } -+ } else { -+ this.opacity = sensitive ? 255 : 0; -+ this.visible = sensitive; -+ } -+ } -+ + _updateOrnament() { + for (const menuItem of this._items.values()) + menuItem.setOrnament(PopupMenu.Ornament.NO_DOT); + + for (const itemKey of this._activeItems) { -+ const menuItem = this._getMenuItem(JSON.parse(itemKey)); ++ const menuItem = this._items.get(itemKey); + if (menuItem) + menuItem.setOrnament(PopupMenu.Ornament.DOT); + } @@ -2725,6 +3030,9 @@ index 0000000000..9b7cebf6d8 + if (a == null || b == null) + return false; + ++ if (typeof a !== 'object' || typeof b !== 'object') ++ return false; ++ + const keysA = Object.keys(a); + const keysB = Object.keys(b); + @@ -2749,9 +3057,6 @@ index 0000000000..9b7cebf6d8 + + let criteriaMismatch = false; + for (const key of Object.keys(searchCriteria)) { -+ if (!searchCriteria[key]) -+ continue; -+ + if (this._deepEquals(item[key], searchCriteria[key])) + continue; + @@ -2791,8 +3096,8 @@ index 0000000000..9b7cebf6d8 + } + }); + -+ this._updateVisibility(); -+ this.updateSensitivity(this._sensitive); ++ this._updateOrnament(); ++ this.updateVisibility(); + } + + addItem(item) { @@ -2812,8 +3117,7 @@ index 0000000000..9b7cebf6d8 + section.addMenuItem(menuItem); + + this._items.set(itemKey, menuItem); -+ this._updateVisibility(); -+ this.updateSensitivity(this._sensitive); ++ this.updateVisibility(); + } + + _getSection(sectionName) { @@ -2849,13 +3153,46 @@ index 0000000000..9b7cebf6d8 + return section; + } + -+ _updateVisibility() { ++ _canBeVisible() { + const visibleSections = this._getVisibleSections(); + + for (const [sectionName, section] of this._sections) + section.actor.visible = visibleSections.includes(sectionName); + -+ this.visible = visibleSections.length > 0; ++ return visibleSections.length > 0; ++ } ++ ++ updateVisibility({visible = true} = {}) { ++ visible &&= this._canBeVisible(); ++ ++ if (this._targetVisibility === visible) ++ return; ++ ++ this._targetVisibility = visible; ++ ++ if (this.animateVisibility) { ++ if (visible) { ++ this.opacity = 0; ++ this.visible = true; ++ this.ease({ ++ opacity: 255, ++ duration: VISIBILITY_ANIMATION_TIME, ++ mode: Clutter.AnimationMode.EASE_OUT_QUAD, ++ }); ++ } else { ++ this.ease({ ++ opacity: 0, ++ duration: VISIBILITY_ANIMATION_TIME, ++ mode: Clutter.AnimationMode.EASE_OUT_QUAD, ++ onComplete: () => { ++ this.visible = false; ++ }, ++ }); ++ } ++ } else { ++ this.opacity = visible ? 255 : 0; ++ this.visible = visible; ++ } + } + + _getVisibleSections() { @@ -2871,11 +3208,11 @@ index 0000000000..9b7cebf6d8 + _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]); @@ -2883,7 +3220,11 @@ index 0000000000..9b7cebf6d8 + } + + 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)) @@ -2925,8 +3266,12 @@ index 0000000000..9b7cebf6d8 + } + + constructor(params = {}) { -+ params.readOnly ??= true; -+ super(params); ++ super({ ++ readOnly: true, ++ ...params, ++ }); ++ ++ this._createChild(); + + this.add_style_class_name('login-dialog-auth-menu-button-indicator'); + } @@ -2954,7 +3299,14 @@ index 0000000000..9b7cebf6d8 + null); + container.add_child(this._descriptionLabel); + -+ return container; ++ this.child = container; ++ } ++ ++ _onClicked() { ++ // When there's only one item, all the info (icon, description) is ++ // already visible in the button, so no need to show the menu ++ if (this._getVisibleItemsCount() > 1) ++ this._menu.toggle(); + } + + addItem(item) { @@ -2989,11 +3341,12 @@ index 0000000000..9b7cebf6d8 + } + + // Override to force visibility even when there's only one item -+ _updateVisibility() { ++ _canBeVisible() { ++ return this._items.size > 0; + } +} diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml -index ec6dee70b2..105076b40e 100644 +index ec6dee70b..105076b40 100644 --- a/js/js-resources.gresource.xml +++ b/js/js-resources.gresource.xml @@ -2,6 +2,7 @@ @@ -3005,46 +3358,45 @@ index ec6dee70b2..105076b40e 100644 gdm/batch.js gdm/constants.js -- -2.51.0 +2.55.0 -From b016701668a727633c2a0684d72d12861da03597 Mon Sep 17 00:00:00 2001 +From f916944b571fcd01b22b060393fc959ad04850f1 Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Tue, 6 Feb 2024 11:11:32 -0500 -Subject: [PATCH 31/48] loginDialog: Port sessions menu over to AuthMenuButton +Subject: [PATCH 36/73] 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: --- - js/gdm/loginDialog.js | 178 +++++++++++++++--------------------------- - 1 file changed, 64 insertions(+), 114 deletions(-) + js/gdm/loginDialog.js | 193 +++++++++++++++--------------------------- + 1 file changed, 67 insertions(+), 126 deletions(-) diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js -index 8b0402ffdc..c85706f1d4 100644 +index 24f3e0a4f..9516a7e89 100644 --- a/js/gdm/loginDialog.js +++ b/js/gdm/loginDialog.js -@@ -20,9 +20,9 @@ +@@ -20,6 +20,7 @@ const { AccountsService, Atk, Clutter, Gdm, Gio, GLib, GObject, Meta, Pango, Shell, St } = imports.gi; +const AuthMenuButton = imports.gdm.authMenuButton; const AuthPrompt = imports.gdm.authPrompt; const Batch = imports.gdm.batch; --const BoxPointer = imports.ui.boxpointer; - const CtrlAltTab = imports.ui.ctrlAltTab; - const GdmUtil = imports.gdm.util; - const Layout = imports.ui.layout; -@@ -36,6 +36,7 @@ const _FADE_ANIMATION_TIME = 250; + const BoxPointer = imports.ui.boxpointer; +@@ -36,6 +37,7 @@ 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 _SESSION_TYPE_SECTION_NAME = _('Session Type'); var UserListItem = GObject.registerClass({ Signals: { 'activate': {} }, -@@ -305,102 +306,6 @@ var UserList = GObject.registerClass({ +@@ -305,102 +307,6 @@ var UserList = GObject.registerClass({ } }); @@ -3147,7 +3499,7 @@ index 8b0402ffdc..c85706f1d4 100644 var LoginDialog = GObject.registerClass({ Signals: { 'failed': {}, -@@ -453,6 +358,7 @@ var LoginDialog = GObject.registerClass({ +@@ -453,6 +359,7 @@ var 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)); @@ -3155,7 +3507,7 @@ index 8b0402ffdc..c85706f1d4 100644 this._authPrompt.hide(); this.add_child(this._authPrompt); -@@ -500,14 +406,7 @@ var LoginDialog = GObject.registerClass({ +@@ -500,14 +407,7 @@ var LoginDialog = GObject.registerClass({ }); this.add_child(this._bottomButtonGroup); @@ -3171,19 +3523,17 @@ index 8b0402ffdc..c85706f1d4 100644 this._logoBin = new St.Widget({ style_class: 'login-dialog-logo-bin', x_align: Clutter.ActorAlign.CENTER, -@@ -538,6 +437,54 @@ var LoginDialog = GObject.registerClass({ +@@ -538,6 +438,56 @@ var LoginDialog = GObject.registerClass({ this._updateDisableUserList.bind(this)); } + _createAuthMenuButton() { + this._authMenuButton = new AuthMenuButton.AuthMenuButton({ -+ title: _('Login Options'), -+ iconName: 'cog-wheel-symbolic', ++ accessible_name: _('Login Options'), + sectionOrder: [_SESSION_TYPE_SECTION_NAME], ++ animateVisibility: true, ++ visible: false, + }); -+ this._authMenuButton.updateSensitivity(false); -+ -+ this._updateSessions(); + + this._authMenuButton.connect('active-item-changed', (_button, sectionName) => { + const item = this._authMenuButton.getActiveItem({sectionName}); @@ -3203,8 +3553,12 @@ index 8b0402ffdc..c85706f1d4 100644 + sectionName: _SESSION_TYPE_SECTION_NAME, + }); + ++ // User is already logged in, it can't choose a different session ++ if (this._user && this._user.is_loaded && this._user.is_logged_in()) ++ return; ++ + const ids = Gdm.get_session_ids(); -+ if (ids.length <= 1) ++ if (ids.length === 1) + return; + + const sessions = ids.map(id => { @@ -3226,19 +3580,19 @@ index 8b0402ffdc..c85706f1d4 100644 _getBannerAllocation(dialogBox) { let actorBox = new Clutter.ActorBox(); -@@ -865,10 +812,8 @@ var LoginDialog = GObject.registerClass({ +@@ -865,10 +815,8 @@ var LoginDialog = GObject.registerClass({ } _onPrompted() { - const showSessionMenu = this._shouldShowSessionMenuButton(); -+ this._authMenuButton.updateSensitivity(this._shouldShowAuthMenu()); ++ this._authMenuButton.updateVisibility({visible: true}); - this._sessionMenuButton.updateSensitivity(showSessionMenu); - this._sessionMenuButton.visible = showSessionMenu; this._showPrompt(); } -@@ -890,7 +835,6 @@ var LoginDialog = GObject.registerClass({ +@@ -890,7 +838,6 @@ var LoginDialog = GObject.registerClass({ _onReset(authPrompt, resetType) { this._resetGreeterProxy(); @@ -3246,54 +3600,80 @@ index 8b0402ffdc..c85706f1d4 100644 const previousUser = this._user; this._user = null; -@@ -924,11 +868,18 @@ var LoginDialog = GObject.registerClass({ +@@ -923,23 +870,15 @@ var LoginDialog = GObject.registerClass({ }); } -+ _onLoading(_authPrompt, isLoading) { -+ this._authMenuButton.updateReactive(!isLoading); -+ } -+ - _onDefaultSessionChanged(client, sessionId) { +- _onDefaultSessionChanged(client, sessionId) { - this._sessionMenuButton.setActiveSession(sessionId); ++ _onLoading(_authPrompt, isLoading) { ++ this._authMenuButton.reactive = !isLoading; + } + +- _shouldShowSessionMenuButton() { +- const visibleStatuses = [ +- AuthPrompt.AuthPromptStatus.VERIFYING, +- AuthPrompt.AuthPromptStatus.VERIFICATION_FAILED, +- AuthPrompt.AuthPromptStatus.VERIFICATION_IN_PROGRESS, +- ]; +- if (!visibleStatuses.includes(this._authPrompt.verificationStatus)) +- return false; +- +- if (this._user && this._user.is_loaded && this._user.is_logged_in()) +- return false; +- +- return true; ++ _onDefaultSessionChanged(client, sessionId) { + this._authMenuButton.setActiveItem({ + sectionName: _SESSION_TYPE_SECTION_NAME, + id: sessionId, + }); } -- _shouldShowSessionMenuButton() { -+ _shouldShowAuthMenu() { - const visibleStatuses = [ - AuthPrompt.AuthPromptStatus.VERIFYING, - AuthPrompt.AuthPromptStatus.VERIFICATION_FAILED, -@@ -990,7 +941,7 @@ var LoginDialog = GObject.registerClass({ + _showPrompt() { +@@ -984,13 +923,14 @@ var LoginDialog = GObject.registerClass({ + this._authPrompt.updateSensitivity({sensitive: false}); + const answer = this._authPrompt.getAnswer(); + this._user = this._userManager.get_user(answer); ++ this._updateSessions(); + this._authPrompt.clear(); + this._authPrompt.begin({ userName: answer }); + this._updateCancelButton(); }); this._updateCancelButton(); - this._sessionMenuButton.updateSensitivity(false); -+ this._authMenuButton.updateSensitivity(false); - this._authPrompt.updateSensitivity({sensitive: true}); ++ this._authMenuButton.updateVisibility({visible: false}); this._showPrompt(); } -@@ -1213,8 +1164,7 @@ var LoginDialog = GObject.registerClass({ + +@@ -1212,8 +1152,7 @@ var LoginDialog = GObject.registerClass({ this._ensureUserListLoaded(); this._authPrompt.hide(); this._hideBannerView(); - this._sessionMenuButton.close(); - this._sessionMenuButton.hide(); -+ this._authMenuButton.updateSensitivity(false); ++ this._authMenuButton.updateVisibility({visible: false}); this._setUserListExpanded(true); this._notListedButton.show(); this._userList.grab_key_focus(); +@@ -1232,6 +1171,8 @@ var LoginDialog = GObject.registerClass({ + _onUserListActivated(activatedItem) { + this._user = activatedItem.user; + ++ this._updateSessions(); ++ + this._updateCancelButton(); + + let batch = new Batch.ConcurrentBatch(this, [GdmUtil.cloneAndFadeOutActor(this._userSelectionBox), -- -2.51.0 +2.55.0 -From 22aca4284e6e61d2e7ddede78a96484a3bab1225 Mon Sep 17 00:00:00 2001 +From 6a2b68a4bdb8e0dcbcf6e0ed9794558bab0e66ff Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Tue, 6 Feb 2024 13:40:26 -0500 -Subject: [PATCH 32/48] loginDialog: Add login options menu to AuthMenuButton +Subject: [PATCH 37/73] loginDialog: Add login options menu to AuthMenuButton MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @@ -3314,35 +3694,34 @@ 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 | 22 +++++++++++++++++++++- + js/gdm/authPrompt.js | 20 ++++++++++++++++++++ js/gdm/loginDialog.js | 43 +++++++++++++++++++++++++++++++++++++++++-- js/gdm/util.js | 5 +++++ - 3 files changed, 67 insertions(+), 3 deletions(-) + 3 files changed, 66 insertions(+), 2 deletions(-) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index 0aca776324..74828a2c42 100644 +index ed7a5f084..86bd2ea8d 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -49,7 +49,8 @@ var AuthPrompt = GObject.registerClass({ +@@ -48,6 +48,7 @@ var AuthPrompt = GObject.registerClass({ 'failed': {}, 'next': {}, 'prompted': {}, -- 'reset': { param_types: [GObject.TYPE_UINT] }, + 'mechanisms-changed': {param_types: [GObject.TYPE_JSOBJECT, GObject.TYPE_JSOBJECT]}, -+ 'reset': {param_types: [GObject.TYPE_UINT]}, + 'reset': { param_types: [GObject.TYPE_UINT] }, 'verification-complete': {}, 'loading': {param_types: [GObject.TYPE_BOOLEAN]}, - }, -@@ -84,6 +85,7 @@ var AuthPrompt = GObject.registerClass({ - 'ask-question', this._onAskQuestion.bind(this), - 'show-message', this._onShowMessage.bind(this), - 'show-choice-list', this._onShowChoiceList.bind(this), +@@ -83,6 +84,7 @@ var 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), -@@ -775,6 +777,24 @@ var AuthPrompt = GObject.registerClass({ + 'verification-failed', (_, ...args) => this._onVerificationFailed(...args), + 'verification-complete', () => this._onVerificationComplete(), + 'reset', (_, ...args) => this._onReset(...args), +@@ -756,6 +758,24 @@ var AuthPrompt = GObject.registerClass({ this.updateSensitivity(false); } @@ -3368,18 +3747,18 @@ index 0aca776324..74828a2c42 100644 let {reuseEntryText, softReset} = Params.parse(params, { reuseEntryText: false, diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js -index c85706f1d4..ab085fae49 100644 +index 9516a7e89..150f74e48 100644 --- a/js/gdm/loginDialog.js +++ b/js/gdm/loginDialog.js -@@ -36,6 +36,7 @@ const _FADE_ANIMATION_TIME = 250; +@@ -37,6 +37,7 @@ 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 _PRIMARY_LOGIN_METHOD_SECTION_NAME = _('Login Options'); const _SESSION_TYPE_SECTION_NAME = _('Session Type'); var UserListItem = GObject.registerClass({ -@@ -359,6 +360,7 @@ var LoginDialog = GObject.registerClass({ +@@ -360,6 +361,7 @@ var LoginDialog = GObject.registerClass({ this._authPrompt.connect('reset', this._onReset.bind(this)); this._authPrompt.connect('verification-complete', this._onVerificationComplete.bind(this)); this._authPrompt.connect('loading', this._onLoading.bind(this)); @@ -3388,15 +3767,15 @@ index c85706f1d4..ab085fae49 100644 this.add_child(this._authPrompt); @@ -441,7 +443,7 @@ var LoginDialog = GObject.registerClass({ + _createAuthMenuButton() { this._authMenuButton = new AuthMenuButton.AuthMenuButton({ - title: _('Login Options'), - iconName: 'cog-wheel-symbolic', + accessible_name: _('Login Options'), - sectionOrder: [_SESSION_TYPE_SECTION_NAME], + sectionOrder: [_PRIMARY_LOGIN_METHOD_SECTION_NAME, _SESSION_TYPE_SECTION_NAME], + animateVisibility: true, + visible: false, }); - this._authMenuButton.updateSensitivity(false); - -@@ -452,7 +454,9 @@ var LoginDialog = GObject.registerClass({ +@@ -451,7 +453,9 @@ var LoginDialog = GObject.registerClass({ if (!item) return; @@ -3407,7 +3786,7 @@ index c85706f1d4..ab085fae49 100644 this._greeter.call_select_session_sync(item.id, null); this._authMenuButton.closeMenu(); -@@ -485,6 +489,20 @@ var LoginDialog = GObject.registerClass({ +@@ -488,6 +492,20 @@ var LoginDialog = GObject.registerClass({ } } @@ -3428,8 +3807,8 @@ index c85706f1d4..ab085fae49 100644 _getBannerAllocation(dialogBox) { let actorBox = new Clutter.ActorBox(); -@@ -872,6 +890,27 @@ var LoginDialog = GObject.registerClass({ - this._authMenuButton.updateReactive(!isLoading); +@@ -874,6 +892,27 @@ var LoginDialog = GObject.registerClass({ + this._authMenuButton.reactive = !isLoading; } + _onMechanismsChanged(_authPrompt, mechanisms, selectedMechanism) { @@ -3457,14 +3836,14 @@ index c85706f1d4..ab085fae49 100644 this._authMenuButton.setActiveItem({ sectionName: _SESSION_TYPE_SECTION_NAME, diff --git a/js/gdm/util.js b/js/gdm/util.js -index 691442ad90..0ff74b4fa1 100644 +index 2a5bb4804..973835b87 100644 --- a/js/gdm/util.js +++ b/js/gdm/util.js -@@ -244,6 +244,11 @@ var ShellUserVerifier = class { +@@ -245,6 +245,11 @@ var ShellUserVerifier = class { this._getUserVerifier(); } -+ selectMechanism(mechanism) { ++ selectMechanism() { + // TODO: Implement mechanism selection + return false; + } @@ -3473,13 +3852,72 @@ index 691442ad90..0ff74b4fa1 100644 if (this._cancellable) this._cancellable.cancel(); -- -2.51.0 +2.55.0 -From 5794aa112c8fab1ed14fa68758a74e6d4a714aad Mon Sep 17 00:00:00 2001 +From c9be5ec36d8286f987332affa81e6a88dcf5242c Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Fri, 3 Jul 2026 13:54:39 +0200 +Subject: [PATCH 38/73] 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 150f74e48..2d249852e 100644 +--- a/js/gdm/loginDialog.js ++++ b/js/gdm/loginDialog.js +@@ -856,6 +856,7 @@ var LoginDialog = GObject.registerClass({ + + _onReset(authPrompt, resetType) { + this._resetGreeterProxy(); ++ this._showBottomButtonGroup(); + + const previousUser = this._user; + this._user = null; +@@ -879,7 +880,16 @@ var 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, +@@ -888,6 +898,10 @@ var LoginDialog = GObject.registerClass({ + }); + } + ++ _onVerificationComplete() { ++ this._hideBottomButtonGroup(); ++ } ++ + _onLoading(_authPrompt, isLoading) { + this._authMenuButton.reactive = !isLoading; + } +-- +2.55.0 + + +From 9adf6d95c795dca9f23058788756027b4b206270 Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Tue, 6 Feb 2024 13:41:39 -0500 -Subject: [PATCH 33/48] unlockDialog: Add _authMenuButton and +Subject: [PATCH 39/73] unlockDialog: Add _authMenuButton and _authIndicatorButton _authMenuButton is used to select an available auth mechanism from the @@ -3490,36 +3928,30 @@ 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. -Only show _otherUserButton when clock is dismissed and authentication -prompt visible. + +Part-of: --- - js/ui/unlockDialog.js | 157 +++++++++++++++++++++++++++++++++++++----- - 1 file changed, 140 insertions(+), 17 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 07b2acaf45..306b64b55d 100644 +index 6ce8e2f41..9b3833297 100644 --- a/js/ui/unlockDialog.js +++ b/js/ui/unlockDialog.js -@@ -4,6 +4,9 @@ - const { AccountsService, Atk, Clutter, Gdm, Gio, - GnomeDesktop, GLib, GObject, Meta, Shell, St } = imports.gi; +@@ -10,7 +10,12 @@ const Main = imports.ui.main; + const MessageTray = imports.ui.messageTray; + const SwipeTracker = imports.ui.swipeTracker; +const AuthMenuButton = imports.gdm.authMenuButton; + const AuthPrompt = imports.gdm.authPrompt; +const GdmConstants = imports.gdm.constants; +const GdmUtil = imports.gdm.util; - const Background = imports.ui.background; - const Layout = imports.ui.layout; - const Main = imports.ui.main; -@@ -12,6 +15,8 @@ const SwipeTracker = imports.ui.swipeTracker; - - const AuthPrompt = imports.gdm.authPrompt; - -+const PRIMARY_UNLOCK_METHOD_SECTION_NAME = _('Unlock Options'); + ++const PRIMARY_UNLOCK_METHOD_SECTION_NAME = _('Unlock Options'); + // The timeout before going back automatically to the lock screen (in seconds) const IDLE_TIMEOUT = 2 * 60; - -@@ -404,12 +409,13 @@ class UnlockDialogClock extends St.BoxLayout { +@@ -400,12 +405,13 @@ class UnlockDialogClock extends St.BoxLayout { var UnlockDialogLayout = GObject.registerClass( class UnlockDialogLayout extends Clutter.LayoutManager { @@ -3535,7 +3967,7 @@ index 07b2acaf45..306b64b55d 100644 } vfunc_get_preferred_width(container, forHeight) { -@@ -469,22 +475,40 @@ class UnlockDialogLayout extends Clutter.LayoutManager { +@@ -457,22 +463,40 @@ class UnlockDialogLayout extends Clutter.LayoutManager { this._stack.allocate(actorBox); @@ -3543,19 +3975,37 @@ index 07b2acaf45..306b64b55d 100644 - if (this._switchUserButton.visible) { - let [, , natWidth, natHeight] = - this._switchUserButton.get_preferred_size(); -+ // Auth Indicator button (left bottom) ++ // Auth Indicator button (bottom start) + if (this._authIndicatorButton.visible) { + const [, , natWidth, natHeight] = + this._authIndicatorButton.get_preferred_size(); ++ ++ const textDirection = this._authIndicatorButton.get_text_direction(); ++ if (textDirection === Clutter.TextDirection.RTL) ++ actorBox.x1 = box.x2 - natWidth; ++ else ++ actorBox.x1 = box.x1; ++ ++ actorBox.y1 = box.y2 - natHeight; ++ actorBox.x2 = actorBox.x1 + natWidth; ++ actorBox.y2 = actorBox.y1 + natHeight; ++ ++ this._authIndicatorButton.allocate(actorBox); ++ } ++ ++ // bottom button group, (has login options and switch user buttons) (bottom end) ++ if (this._bottomButtonGroup.visible) { ++ const [, , natWidth, natHeight] = ++ this._bottomButtonGroup.get_preferred_size(); - const textDirection = this._switchUserButton.get_text_direction(); -+ const textDirection = this._authIndicatorButton.get_text_direction(); ++ const textDirection = this._bottomButtonGroup.get_text_direction(); if (textDirection === Clutter.TextDirection.RTL) - actorBox.x1 = box.x1 + natWidth; -+ actorBox.x1 = box.x2 - natWidth; ++ actorBox.x1 = box.x1; else - actorBox.x1 = box.x2 - (natWidth * 2); -+ actorBox.x1 = box.x1; ++ actorBox.x1 = box.x2 - natWidth; - actorBox.y1 = box.y2 - (natHeight * 2); + actorBox.y1 = box.y2 - natHeight; @@ -3563,35 +4013,18 @@ index 07b2acaf45..306b64b55d 100644 actorBox.y2 = actorBox.y1 + natHeight; - this._switchUserButton.allocate(actorBox); -+ this._authIndicatorButton.allocate(actorBox); -+ } -+ -+ // bottom button group, (has login options and switch user buttons) (right bottom) -+ if (this._bottomButtonGroup.visible) { -+ const [, , natWidth, natHeight] = -+ this._bottomButtonGroup.get_preferred_size(); -+ -+ const textDirection = this._bottomButtonGroup.get_text_direction(); -+ if (textDirection === Clutter.TextDirection.RTL) -+ actorBox.x1 = box.x1; -+ else -+ actorBox.x1 = box.x2 - natWidth; -+ -+ actorBox.y1 = box.y2 - natHeight; -+ actorBox.x2 = actorBox.x1 + natWidth; -+ actorBox.y2 = actorBox.y1 + natHeight; -+ + this._bottomButtonGroup.allocate(actorBox); } } }); -@@ -589,19 +613,47 @@ var UnlockDialog = GObject.registerClass({ +@@ -577,19 +601,47 @@ var UnlockDialog = GObject.registerClass({ this._notificationsBox = new NotificationsBox(); this._notificationsBox.connect('wake-up-screen', () => this.emit('wake-up-screen')); + this._bottomButtonGroup = new St.BoxLayout({ + style_class: 'login-dialog-bottom-button-group', + }); ++ this._bottomButtonGroup.set_pivot_point(0.5, 0.5); + // Switch User button this._otherUserButton = new St.Button({ @@ -3605,14 +4038,15 @@ index 07b2acaf45..306b64b55d 100644 - child: new St.Icon({ icon_name: 'system-users-symbolic' }), + label: _('Switch User…'), }); - this._otherUserButton.set_pivot_point(0.5, 0.5); +- this._otherUserButton.set_pivot_point(0.5, 0.5); this._otherUserButton.connect('clicked', this._otherUserClicked.bind(this)); + this._bottomButtonGroup.add_child(this._otherUserButton); + + // Login Options button + this._authMenuButton = new AuthMenuButton.AuthMenuButton({ -+ title: _('Login Options'), -+ iconName: 'cog-wheel-symbolic', ++ accessible_name: _('Login Options'), ++ visible: false, ++ y_align: Clutter.ActorAlign.END, + }); + this._authMenuButton.connect('active-item-changed', () => { + const authMechanism = this._authMenuButton.getActiveItem(); @@ -3622,20 +4056,19 @@ index 07b2acaf45..306b64b55d 100644 + this._selectAuthMechanism(authMechanism); + this._authMenuButton.closeMenu(); + }); -+ this._authMenuButton.updateSensitivity(true); + this._bottomButtonGroup.add_child(this._authMenuButton); + + // Auth Indicators + this._authIndicatorButton = new AuthMenuButton.AuthMenuButtonIndicator({ -+ title: _('Background Authentication Methods'), ++ accessible_name: _('Background Authentication Methods'), + animateVisibility: true, ++ visible: false, + }); + this._authIndicatorButton.set_pivot_point(0.5, 0.5); -+ this._authIndicatorButton.updateSensitivity(true); this._screenSaverSettings = new Gio.Settings({ schema_id: 'org.gnome.desktop.screensaver' }); -@@ -618,11 +670,13 @@ var UnlockDialog = GObject.registerClass({ +@@ -606,11 +658,13 @@ var UnlockDialog = GObject.registerClass({ mainBox.add_constraint(new Layout.MonitorConstraint({ primary: true })); mainBox.add_child(this._stack); mainBox.add_child(this._notificationsBox); @@ -3651,7 +4084,7 @@ index 07b2acaf45..306b64b55d 100644 this.add_child(mainBox); this._idleMonitor = Meta.IdleMonitor.get_core(); -@@ -653,6 +707,20 @@ var UnlockDialog = GObject.registerClass({ +@@ -641,6 +695,20 @@ var UnlockDialog = GObject.registerClass({ return Clutter.EVENT_PROPAGATE; } @@ -3672,7 +4105,7 @@ index 07b2acaf45..306b64b55d 100644 _createBackground(monitorIndex) { let monitor = Main.layoutManager.monitors[monitorIndex]; let widget = new St.Widget({ -@@ -709,6 +777,8 @@ var UnlockDialog = GObject.registerClass({ +@@ -697,6 +765,8 @@ var 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)); @@ -3681,7 +4114,15 @@ index 07b2acaf45..306b64b55d 100644 this._promptBox.add_child(this._authPrompt); } -@@ -771,6 +841,12 @@ var UnlockDialog = GObject.registerClass({ +@@ -721,6 +791,7 @@ var UnlockDialog = GObject.registerClass({ + if (this._authPrompt) { + this._authPrompt.destroy(); + this._authPrompt = null; ++ this._authIndicatorButton.clearItems(); + } + } + +@@ -759,6 +830,12 @@ var UnlockDialog = GObject.registerClass({ reactive: progress > 0, can_focus: progress > 0, }); @@ -3694,7 +4135,7 @@ index 07b2acaf45..306b64b55d 100644 const { scaleFactor } = St.ThemeContext.get_for_stage(global.stage); -@@ -788,7 +864,7 @@ var UnlockDialog = GObject.registerClass({ +@@ -776,7 +853,7 @@ var UnlockDialog = GObject.registerClass({ translation_y: -FADE_OUT_TRANSLATION * progress * scaleFactor, }); @@ -3703,12 +4144,12 @@ index 07b2acaf45..306b64b55d 100644 opacity: 255 * progress, scale_x: FADE_OUT_SCALE + (1 - FADE_OUT_SCALE) * progress, scale_y: FADE_OUT_SCALE + (1 - FADE_OUT_SCALE) * progress, -@@ -812,6 +888,52 @@ var UnlockDialog = GObject.registerClass({ +@@ -800,6 +877,52 @@ var UnlockDialog = GObject.registerClass({ this._authPrompt.begin({ userName }); } + _onLoading(_authPrompt, isLoading) { -+ this._authMenuButton.updateReactive(!isLoading); ++ this._authMenuButton.reactive = !isLoading; + } + + _onMechanismsChanged(_authPrompt, mechanisms, selectedMechanism) { @@ -3756,7 +4197,7 @@ index 07b2acaf45..306b64b55d 100644 _escape() { if (this._authPrompt && this.allowCancel) this._authPrompt.cancel(); -@@ -893,7 +1015,8 @@ var UnlockDialog = GObject.registerClass({ +@@ -881,7 +1004,8 @@ var UnlockDialog = GObject.registerClass({ _updateUserSwitchVisibility() { this._otherUserButton.visible = this._userManager.can_switch() && @@ -3767,35 +4208,43 @@ index 07b2acaf45..306b64b55d 100644 cancel() { -- -2.51.0 +2.55.0 -From 3725f050c3be47873916e27a5ddee471b16d3764 Mon Sep 17 00:00:00 2001 +From 2907be0148ecd89ed632dbca8b8ab46ba4873202 Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Tue, 3 Dec 2024 07:39:32 -0500 -Subject: [PATCH 34/48] unlockDialog: Update hint text based on mockup +Subject: [PATCH 40/73] 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 | 14 +++++++++++--- - 1 file changed, 11 insertions(+), 3 deletions(-) + 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 306b64b55d..db6f1d4ba1 100644 +index 9b3833297..5ecb7ed4b 100644 --- a/js/ui/unlockDialog.js +++ b/js/ui/unlockDialog.js -@@ -393,9 +393,17 @@ class UnlockDialogClock extends St.BoxLayout { +@@ -388,10 +388,23 @@ class UnlockDialogClock extends St.BoxLayout { + this._date.text = date.toLocaleFormat(dateFormat); } ++ selectAuthRole(roleName) { ++ this._selectedAuthRole = roleName; ++ this._updateHint(); ++ } ++ _updateHint() { - this._hint.text = this._seat.touch_mode - ? _('Swipe up to unlock') - : _('Click or press a key to unlock'); -+ const authMechanism = this._selectedAuthMechanism; ++ const selectedAuthRole = this._selectedAuthRole; + let text; + -+ if (authMechanism?.role === GdmConstants.SMARTCARD_ROLE_NAME) ++ if (selectedAuthRole === GdmConstants.SMARTCARD_ROLE_NAME) + text = _('Insert smartcard'); + else if (this._seat.touch_mode) + text = _('Swipe up'); @@ -3806,31 +4255,42 @@ index 306b64b55d..db6f1d4ba1 100644 } _onDestroy() { +@@ -707,6 +720,8 @@ var UnlockDialog = GObject.registerClass({ + } + + this._selectedAuthMechanism = authMechanism; ++ ++ this._clock.selectAuthRole(authMechanism?.role); + } + + _createBackground(monitorIndex) { -- -2.51.0 +2.55.0 -From 01988c6b13a40b2383edaf3cca1be0bc402e3102 Mon Sep 17 00:00:00 2001 +From 27ecabb170961c0249a29058bc6e786b9035a75e Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Tue, 14 Oct 2025 19:19:15 +0200 -Subject: [PATCH 35/48] gdm/util: Increase time of messages based on new +Subject: [PATCH 41/73] 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 0ff74b4fa1..ae35e5bc5c 100644 +index 973835b87..53656cf4b 100644 --- a/js/gdm/util.js +++ b/js/gdm/util.js -@@ -46,6 +46,11 @@ var DISABLE_USER_LIST_KEY = 'disable-user-list'; +@@ -47,6 +47,11 @@ var DISABLE_USER_LIST_KEY = 'disable-user-list'; // Give user 48ms to read each character of a PAM message var USER_READ_TIME = 48; -+const MESSAGE_TIME_MULTIPLIER = (() => { ++var MESSAGE_TIME_MULTIPLIER = (() => { + const value = Number.parseFloat(GLib.getenv('GDM_MESSAGE_TIME_MULTIPLIER')); + return Number.isFinite(value) && value > 0 ? value : 1; +})(); @@ -3838,7 +4298,7 @@ index 0ff74b4fa1..ae35e5bc5c 100644 const FINGERPRINT_SERVICE_PROXY_TIMEOUT = 5000; const FINGERPRINT_ERROR_TIMEOUT_WAIT = 15; -@@ -323,7 +328,7 @@ var ShellUserVerifier = class { +@@ -324,7 +329,7 @@ var ShellUserVerifier = class { return 0; // We probably could be smarter here @@ -3848,13 +4308,13 @@ index 0ff74b4fa1..ae35e5bc5c 100644 finishMessageQueue() { -- -2.51.0 +2.55.0 -From 8329dbcdeaa0f5dc1b3e37a994d7396a3b508ce1 Mon Sep 17 00:00:00 2001 +From 356f12bc39126e3bcffccfb7d91c9efaacf772bb Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Mon, 15 Sep 2025 17:13:17 +0200 -Subject: [PATCH 36/48] gdm/util: Allow null _hold and don't recreate dummy +Subject: [PATCH 42/73] gdm/util: Allow null _hold and don't recreate dummy holds _hold property is used to inform the caller of begin method (authPrompt) @@ -3867,18 +4327,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 74828a2c42..a7ec1dcda4 100644 +index 86bd2ea8d..aa36fbb8b 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -868,11 +868,7 @@ var AuthPrompt = GObject.registerClass({ - - this.updateSensitivity({sensitive: false}); +@@ -855,11 +855,7 @@ var AuthPrompt = GObject.registerClass({ + if (!this._preemptiveInput) + this.updateSensitivity({sensitive: false}); - let hold = params.hold; - if (!hold) @@ -3890,19 +4352,23 @@ index 74828a2c42..a7ec1dcda4 100644 } diff --git a/js/gdm/util.js b/js/gdm/util.js -index ae35e5bc5c..e862e2c0e7 100644 +index 53656cf4b..028d4b194 100644 --- a/js/gdm/util.js +++ b/js/gdm/util.js -@@ -519,8 +519,6 @@ var ShellUserVerifier = class { +@@ -519,11 +519,8 @@ var ShellUserVerifier = class { + 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(); - } +- } } -@@ -583,7 +581,7 @@ var ShellUserVerifier = class { + + _setFingerprintReaderType(fprintDeviceType) { +@@ -584,7 +581,7 @@ var ShellUserVerifier = class { _reportInitError(where, error, serviceName) { logError(error, where); @@ -3911,7 +4377,7 @@ index ae35e5bc5c..e862e2c0e7 100644 this._queueMessage(serviceName, _('Authentication error'), MessageType.ERROR); this._failCounter++; -@@ -619,7 +617,7 @@ var ShellUserVerifier = class { +@@ -620,7 +617,7 @@ var ShellUserVerifier = class { this.reauthenticating = true; this._connectSignals(); this._beginVerification(); @@ -3920,7 +4386,7 @@ index ae35e5bc5c..e862e2c0e7 100644 } async _getUserVerifier() { -@@ -641,7 +639,7 @@ var ShellUserVerifier = class { +@@ -642,7 +639,7 @@ var ShellUserVerifier = class { this._connectSignals(); this._beginVerification(); @@ -3929,7 +4395,7 @@ index ae35e5bc5c..e862e2c0e7 100644 } _connectSignals() { -@@ -760,7 +758,7 @@ var ShellUserVerifier = class { +@@ -770,7 +767,7 @@ var ShellUserVerifier = class { } async _startService(serviceName) { @@ -3938,7 +4404,7 @@ index ae35e5bc5c..e862e2c0e7 100644 try { this._activeServices.add(serviceName); if (this._userName) { -@@ -776,7 +774,7 @@ var ShellUserVerifier = class { +@@ -786,7 +783,7 @@ var ShellUserVerifier = class { return; if (!this.serviceIsForeground(serviceName)) { logError(e, 'Failed to start %s for %s'.format(serviceName, this._userName)); @@ -3947,7 +4413,7 @@ index ae35e5bc5c..e862e2c0e7 100644 return; } this._reportInitError(this._userName -@@ -785,7 +783,7 @@ var ShellUserVerifier = class { +@@ -795,7 +792,7 @@ var ShellUserVerifier = class { serviceName); return; } @@ -3956,7 +4422,7 @@ index ae35e5bc5c..e862e2c0e7 100644 } _beginVerification() { -@@ -911,7 +909,6 @@ var ShellUserVerifier = class { +@@ -921,7 +918,6 @@ var ShellUserVerifier = class { } _retry(serviceName) { @@ -3965,13 +4431,13 @@ index ae35e5bc5c..e862e2c0e7 100644 this._startService(serviceName); } -- -2.51.0 +2.55.0 -From 43d82e9611eb899c345b972633ad279a4b71d6df Mon Sep 17 00:00:00 2001 +From 00a4a72b479b08e5577fdd8012127ec395a1c3a3 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Wed, 20 Aug 2025 10:54:33 +0200 -Subject: [PATCH 37/48] misc: Add FingerprintManager +Subject: [PATCH 43/73] gdm: Add FingerprintManager Move fingerprint bits to new fingerprintManager class. @@ -3980,286 +4446,26 @@ functionality inside fingerprintManager. This will be improved in the next commits when moving fingerprint authentication to a specific class instead of util. ---- - js/gdm/util.js | 159 ++++++++-------------------------- - js/js-resources.gresource.xml | 1 + - js/misc/fingerprintManager.js | 138 +++++++++++++++++++++++++++++ - 3 files changed, 174 insertions(+), 124 deletions(-) - create mode 100644 js/misc/fingerprintManager.js -diff --git a/js/gdm/util.js b/js/gdm/util.js -index e862e2c0e7..1176f77500 100644 ---- a/js/gdm/util.js -+++ b/js/gdm/util.js -@@ -7,6 +7,7 @@ const Signals = imports.signals; - - const Batch = imports.gdm.batch; - const Constants = imports.gdm.constants; -+const {FingerprintManager, FingerprintReaderType} = imports.misc.fingerprintManager; - const OVirt = imports.gdm.oVirt; - const Vmware = imports.gdm.vmware; - const Main = imports.ui.main; -@@ -14,11 +15,6 @@ const { loadInterfaceXML } = imports.misc.fileUtils; - const Params = imports.misc.params; - const SmartcardManager = imports.misc.smartcardManager; - --const FprintManagerInfo = Gio.DBusInterfaceInfo.new_for_xml( -- loadInterfaceXML('net.reactivated.Fprint.Manager')); --const FprintDeviceInfo = Gio.DBusInterfaceInfo.new_for_xml( -- loadInterfaceXML('net.reactivated.Fprint.Device')); -- - Gio._promisify(Gdm.Client.prototype, - 'open_reauthentication_channel', 'open_reauthentication_channel_finish'); - Gio._promisify(Gdm.Client.prototype, -@@ -62,12 +58,6 @@ var MessageType = { - ERROR: 3, - }; - --const FingerprintReaderType = { -- NONE: 0, -- PRESS: 1, -- SWIPE: 2, --}; -- - function fadeInActor(actor) { - if (actor.opacity == 255 && actor.visible) - return null; -@@ -187,6 +177,7 @@ var ShellUserVerifier = class { - this._defaultService = null; - this._preemptingService = null; - this._fingerprintReaderType = FingerprintReaderType.NONE; -+ this._fingerprintReaderFound = false; - - this._messageQueue = []; - this._messageQueueTimeoutId = 0; -@@ -238,8 +229,7 @@ var ShellUserVerifier = class { - this._userName = userName; - this.reauthenticating = false; - -- this._checkForFingerprintReader().catch(e => -- this._handleFingerprintError(e)); -+ this._fingerprintManager?.checkReaderType(this._cancellable); - - // If possible, reauthenticate an already running session, - // so any session specific credentials get updated appropriately -@@ -297,6 +287,7 @@ var ShellUserVerifier = class { - this._smartcardManager?.disconnect(this._smartcardRemovedId); - this._smartcardManager = null; - -+ this._fingerprintManager?.disconnectObject(this); - this._fingerprintManager = null; - - for (let service in this._credentialManagers) { -@@ -414,121 +405,39 @@ var ShellUserVerifier = class { - } - - async _initFingerprintManager() { -- if (this._fprintManager) -- return; -- -- const fprintManager = new Gio.DBusProxy({ -- g_connection: Gio.DBus.system, -- g_name: 'net.reactivated.Fprint', -- g_object_path: '/net/reactivated/Fprint/Manager', -- g_interface_name: FprintManagerInfo.name, -- g_interface_info: FprintManagerInfo, -- g_flags: Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES | -- Gio.DBusProxyFlags.DO_NOT_AUTO_START_AT_CONSTRUCTION | -- Gio.DBusProxyFlags.DO_NOT_CONNECT_SIGNALS, -- }); -- -- try { -- if (!this._getDetectedDefaultService()) { -- // Other authentication methods would have already been detected by -- // now as possibilities if they were available. -- // If we're here it means that FINGERPRINT_AUTHENTICATION_KEY is -- // true and so fingerprint authentication is our last potential -- // option, so go ahead a synchronously look for a fingerprint device -- // during startup or default service update. -- fprintManager.init(null); -- // Do not wait too much for fprintd to reply, as in case it hangs -- // we should fail early without having the shell to misbehave -- fprintManager.set_default_timeout(FINGERPRINT_SERVICE_PROXY_TIMEOUT); -- -- const result = fprintManager.call_sync( -- 'GetDefaultDevice', -- null, -- Gio.DBusCallFlags.NONE, -- FINGERPRINT_SERVICE_PROXY_TIMEOUT, -- null); -- const [devicePath] = result.deep_unpack(); -- this._fprintManager = fprintManager; -- -- const fprintDeviceProxy = this._getFingerprintDeviceProxy(devicePath); -- fprintDeviceProxy.init(null); -- this._setFingerprintReaderType(fprintDeviceProxy['scan-type']); -- } else { -- // Ensure fingerprint service starts, but do not wait for it -- const cancellable = this._cancellable; -- await fprintManager.init_async(GLib.PRIORITY_DEFAULT, cancellable); -- await this._updateFingerprintReaderType(fprintManager, cancellable); -- this._fprintManager = fprintManager; -- } -- } catch (e) { -- this._handleFingerprintError(e); -- } -- } -- -- _getFingerprintDeviceProxy(devicePath) { -- return new Gio.DBusProxy({ -- g_connection: Gio.DBus.system, -- g_name: 'net.reactivated.Fprint', -- g_object_path: devicePath, -- g_interface_name: FprintDeviceInfo.name, -- g_interface_info: FprintDeviceInfo, -- g_flags: Gio.DBusProxyFlags.DO_NOT_CONNECT_SIGNALS, -- }); -- } -- -- _handleFingerprintError(e) { -- this._fingerprintReaderType = FingerprintReaderType.NONE; -- -- if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) -- return; -- if (e.matches(Gio.DBusError, Gio.DBusError.SERVICE_UNKNOWN)) -+ if (this._fingerprintManager) - return; - -- if (Gio.DBusError.is_remote_error(e) && -- Gio.DBusError.get_remote_error(e) === -- 'net.reactivated.Fprint.Error.NoSuchDevice') -- return; -- -- logError(e, 'Failed to interact with fprintd service'); -- } -- -- async _checkForFingerprintReader() { -- if (!this._fprintManager) { -- this._updateDefaultService(); -- return; -+ this._fingerprintManager = new FingerprintManager(this._cancellable); -+ this._fingerprintManager.connectObject( -+ 'reader-type-changed', () => this._onFingerprintReaderTypeChanged(), -+ this); -+ -+ if (!this._getDetectedDefaultService()) { -+ // Other authentication methods would have already been detected by -+ // now as possibilities if they were available. -+ // If we're here it means that FINGERPRINT_AUTHENTICATION_KEY is -+ // true and so fingerprint authentication is our last potential -+ // option, so go ahead a synchronously look for a fingerprint device -+ // during startup or default service update. -+ // Do not wait too much for fprintd to reply, as in case it hangs -+ // we should fail early without having the shell to misbehave -+ this._fingerprintManager.setDefaultTimeout(FINGERPRINT_SERVICE_PROXY_TIMEOUT); -+ await this._fingerprintManager.checkReaderType(this._cancellable); -+ } else { -+ // Ensure fingerprint service starts, but do not wait for it -+ this._fingerprintManager.checkReaderType(this._cancellable); - } -- -- if (this._fingerprintReaderType !== FingerprintReaderType.NONE) -- return; -- -- await this._updateFingerprintReaderType(this._fprintManager, this._cancellable); - } - -- async _updateFingerprintReaderType(fprintManager, cancellable) { -- const result = await fprintManager.call( -- 'GetDefaultDevice', -- null, -- Gio.DBusCallFlags.NONE, -- -1, -- cancellable); -- const [devicePath] = result.deep_unpack(); -- const fprintDeviceProxy = this._getFingerprintDeviceProxy(devicePath); -- await fprintDeviceProxy.init_async(GLib.PRIORITY_DEFAULT, cancellable); -- this._setFingerprintReaderType(fprintDeviceProxy['scan-type']); -+ _onFingerprintReaderTypeChanged() { -+ this._fingerprintReaderType = this._fingerprintManager.readerType; -+ this._fingerprintReaderFound = this._fingerprintManager.readerFound; - this._updateDefaultService(); - - if (this._userVerifier && -- !this._activeServices.has(Constants.FINGERPRINT_SERVICE_NAME)) { -- await this._maybeStartFingerprintVerification(); -- } -- } -- -- _setFingerprintReaderType(fprintDeviceType) { -- this._fingerprintReaderType = -- FingerprintReaderType[fprintDeviceType.toUpperCase()]; -- -- if (this._fingerprintReaderType === undefined) -- throw new Error(`Unexpected fingerprint device type '${fprintDeviceType}'`); -+ !this._activeServices.has(Constants.FINGERPRINT_SERVICE_NAME)) -+ this._maybeStartFingerprintVerification(); - } - - _onCredentialManagerAuthenticated(credentialManager, _token) { -@@ -693,7 +602,7 @@ var ShellUserVerifier = class { - } - - serviceIsFingerprint(serviceName) { -- return this._fingerprintReaderType !== FingerprintReaderType.NONE && -+ return this._fingerprintReaderFound && - serviceName === Constants.FINGERPRINT_SERVICE_NAME; - } - -@@ -706,9 +615,11 @@ var ShellUserVerifier = class { - let needsReset = false; - - if (this._settings.get_boolean(FINGERPRINT_AUTHENTICATION_KEY)) { -- this._initFingerprintManager().catch(logError); -+ this._initFingerprintManager(); - } else if (this._fingerprintManager) { -+ this._fingerprintManager.disconnectObject(this); - this._fingerprintManager = null; -+ this._fingerprintReaderFound = false; - this._fingerprintReaderType = FingerprintReaderType.NONE; - - if (this._activeServices.has(Constants.FINGERPRINT_SERVICE_NAME)) -@@ -737,7 +648,7 @@ var ShellUserVerifier = class { - return Constants.PASSWORD_SERVICE_NAME; - else if (this._smartcardManager) - return Constants.SMARTCARD_SERVICE_NAME; -- else if (this._fingerprintReaderType !== FingerprintReaderType.NONE) -+ else if (this._fingerprintReaderFound) - return Constants.FINGERPRINT_SERVICE_NAME; - return null; - } -@@ -793,7 +704,7 @@ var ShellUserVerifier = class { - - async _maybeStartFingerprintVerification() { - if (this._userName && -- this._fingerprintReaderType !== FingerprintReaderType.NONE && -+ this._fingerprintReaderFound && - !this.serviceIsForeground(Constants.FINGERPRINT_SERVICE_NAME)) - await this._startService(Constants.FINGERPRINT_SERVICE_NAME); - } -diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml -index 105076b40e..ad4057d585 100644 ---- a/js/js-resources.gresource.xml -+++ b/js/js-resources.gresource.xml -@@ -15,6 +15,7 @@ - - misc/config.js - misc/extensionUtils.js -+ misc/fingerprintManager.js - misc/fileUtils.js - misc/gnomeSession.js - misc/history.js -diff --git a/js/misc/fingerprintManager.js b/js/misc/fingerprintManager.js +Part-of: +--- + js/gdm/fingerprintManager.js | 138 ++++++++++++++++++++++++++++ + js/gdm/util.js | 163 ++++++++-------------------------- + js/js-resources.gresource.xml | 1 + + 3 files changed, 176 insertions(+), 126 deletions(-) + create mode 100644 js/gdm/fingerprintManager.js + +diff --git a/js/gdm/fingerprintManager.js b/js/gdm/fingerprintManager.js new file mode 100644 -index 0000000000..6ef2d68d3a +index 000000000..2577facbb --- /dev/null -+++ b/js/misc/fingerprintManager.js ++++ b/js/gdm/fingerprintManager.js @@ -0,0 +1,138 @@ +import Gio from 'gi://Gio'; +import GLib from 'gi://GLib'; +import GObject from 'gi://GObject'; + -+import {loadInterfaceXML} from './fileUtils.js'; ++import {loadInterfaceXML} from '../misc/fileUtils.js'; + +const FprintManagerInfo = Gio.DBusInterfaceInfo.new_for_xml( + loadInterfaceXML('net.reactivated.Fprint.Manager')); @@ -4393,43 +4599,306 @@ index 0000000000..6ef2d68d3a + logError(e, 'Failed to interact with fprintd service'); + } +} +diff --git a/js/gdm/util.js b/js/gdm/util.js +index 028d4b194..d12969bac 100644 +--- a/js/gdm/util.js ++++ b/js/gdm/util.js +@@ -8,18 +8,13 @@ const Signals = imports.signals; + + const Batch = imports.gdm.batch; + const Constants = imports.gdm.constants; ++const FingerprintManager = imports.gdm.fingerprintManager; + const OVirt = imports.gdm.oVirt; + const Vmware = imports.gdm.vmware; + const Main = imports.ui.main; +-const { loadInterfaceXML } = imports.misc.fileUtils; + const Params = imports.misc.params; + const SmartcardManager = imports.misc.smartcardManager; + +-const FprintManagerInfo = Gio.DBusInterfaceInfo.new_for_xml( +- loadInterfaceXML('net.reactivated.Fprint.Manager')); +-const FprintDeviceInfo = Gio.DBusInterfaceInfo.new_for_xml( +- loadInterfaceXML('net.reactivated.Fprint.Device')); +- + Gio._promisify(Gdm.Client.prototype, + 'open_reauthentication_channel', 'open_reauthentication_channel_finish'); + Gio._promisify(Gdm.Client.prototype, +@@ -63,12 +58,6 @@ var MessageType = { + ERROR: 3, + }; + +-const FingerprintReaderType = { +- NONE: 0, +- PRESS: 1, +- SWIPE: 2, +-}; +- + function fadeInActor(actor) { + if (actor.opacity == 255 && actor.visible) + return null; +@@ -187,7 +176,8 @@ var ShellUserVerifier = class { + + this._defaultService = null; + this._preemptingService = null; +- this._fingerprintReaderType = FingerprintReaderType.NONE; ++ this._fingerprintReaderType = FingerprintManager.FingerprintReaderType.NONE; ++ this._fingerprintReaderFound = false; + + this._messageQueue = []; + this._messageQueueTimeoutId = 0; +@@ -239,8 +229,7 @@ var ShellUserVerifier = class { + this._userName = userName; + this.reauthenticating = false; + +- this._checkForFingerprintReader().catch(e => +- this._handleFingerprintError(e)); ++ this._fingerprintManager?.checkReaderType(this._cancellable); + + // If possible, reauthenticate an already running session, + // so any session specific credentials get updated appropriately +@@ -298,6 +287,7 @@ var ShellUserVerifier = class { + this._smartcardManager?.disconnect(this._smartcardRemovedId); + this._smartcardManager = null; + ++ this._fingerprintManager?.disconnectObject(this); + this._fingerprintManager = null; + + for (let service in this._credentialManagers) { +@@ -415,120 +405,39 @@ var ShellUserVerifier = class { + } + + async _initFingerprintManager() { +- if (this._fprintManager) +- return; +- +- const fprintManager = new Gio.DBusProxy({ +- g_connection: Gio.DBus.system, +- g_name: 'net.reactivated.Fprint', +- g_object_path: '/net/reactivated/Fprint/Manager', +- g_interface_name: FprintManagerInfo.name, +- g_interface_info: FprintManagerInfo, +- g_flags: Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES | +- Gio.DBusProxyFlags.DO_NOT_AUTO_START_AT_CONSTRUCTION | +- Gio.DBusProxyFlags.DO_NOT_CONNECT_SIGNALS, +- }); +- +- try { +- if (!this._getDetectedDefaultService()) { +- // Other authentication methods would have already been detected by +- // now as possibilities if they were available. +- // If we're here it means that FINGERPRINT_AUTHENTICATION_KEY is +- // true and so fingerprint authentication is our last potential +- // option, so go ahead a synchronously look for a fingerprint device +- // during startup or default service update. +- fprintManager.init(null); +- // Do not wait too much for fprintd to reply, as in case it hangs +- // we should fail early without having the shell to misbehave +- fprintManager.set_default_timeout(FINGERPRINT_SERVICE_PROXY_TIMEOUT); +- +- const result = fprintManager.call_sync( +- 'GetDefaultDevice', +- null, +- Gio.DBusCallFlags.NONE, +- FINGERPRINT_SERVICE_PROXY_TIMEOUT, +- null); +- const [devicePath] = result.deep_unpack(); +- this._fprintManager = fprintManager; +- +- const fprintDeviceProxy = this._getFingerprintDeviceProxy(devicePath); +- fprintDeviceProxy.init(null); +- this._setFingerprintReaderType(fprintDeviceProxy['scan-type']); +- } else { +- // Ensure fingerprint service starts, but do not wait for it +- const cancellable = this._cancellable; +- await fprintManager.init_async(GLib.PRIORITY_DEFAULT, cancellable); +- await this._updateFingerprintReaderType(fprintManager, cancellable); +- this._fprintManager = fprintManager; +- } +- } catch (e) { +- this._handleFingerprintError(e); +- } +- } +- +- _getFingerprintDeviceProxy(devicePath) { +- return new Gio.DBusProxy({ +- g_connection: Gio.DBus.system, +- g_name: 'net.reactivated.Fprint', +- g_object_path: devicePath, +- g_interface_name: FprintDeviceInfo.name, +- g_interface_info: FprintDeviceInfo, +- g_flags: Gio.DBusProxyFlags.DO_NOT_CONNECT_SIGNALS, +- }); +- } +- +- _handleFingerprintError(e) { +- this._fingerprintReaderType = FingerprintReaderType.NONE; +- +- if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) +- return; +- if (e.matches(Gio.DBusError, Gio.DBusError.SERVICE_UNKNOWN)) ++ if (this._fingerprintManager) + return; + +- if (Gio.DBusError.is_remote_error(e) && +- Gio.DBusError.get_remote_error(e) === +- 'net.reactivated.Fprint.Error.NoSuchDevice') +- return; +- +- logError(e, 'Failed to interact with fprintd service'); +- } +- +- async _checkForFingerprintReader() { +- if (!this._fprintManager) { +- this._updateDefaultService(); +- return; ++ this._fingerprintManager = FingerprintManager.getFingerprintManager(); ++ this._fingerprintManager.connectObject( ++ 'reader-type-changed', () => this._onFingerprintReaderTypeChanged(), ++ this); ++ ++ if (!this._getDetectedDefaultService()) { ++ // Other authentication methods would have already been detected by ++ // now as possibilities if they were available. ++ // If we're here it means that FINGERPRINT_AUTHENTICATION_KEY is ++ // true and so fingerprint authentication is our last potential ++ // option, so go ahead a synchronously look for a fingerprint device ++ // during startup or default service update. ++ // Do not wait too much for fprintd to reply, as in case it hangs ++ // we should fail early without having the shell to misbehave ++ this._fingerprintManager.setDefaultTimeout(FINGERPRINT_SERVICE_PROXY_TIMEOUT); ++ await this._fingerprintManager.checkReaderType(this._cancellable); ++ } else { ++ // Ensure fingerprint service starts, but do not wait for it ++ this._fingerprintManager.checkReaderType(this._cancellable); + } +- +- if (this._fingerprintReaderType !== FingerprintReaderType.NONE) +- return; +- +- await this._updateFingerprintReaderType(this._fprintManager, this._cancellable); + } + +- async _updateFingerprintReaderType(fprintManager, cancellable) { +- const result = await fprintManager.call( +- 'GetDefaultDevice', +- null, +- Gio.DBusCallFlags.NONE, +- -1, +- cancellable); +- const [devicePath] = result.deep_unpack(); +- const fprintDeviceProxy = this._getFingerprintDeviceProxy(devicePath); +- await fprintDeviceProxy.init_async(GLib.PRIORITY_DEFAULT, cancellable); +- this._setFingerprintReaderType(fprintDeviceProxy['scan-type']); ++ _onFingerprintReaderTypeChanged() { ++ this._fingerprintReaderType = this._fingerprintManager.readerType; ++ this._fingerprintReaderFound = this._fingerprintManager.readerFound; + this._updateDefaultService(); + + if (this._userVerifier && + !this._activeServices.has(Constants.FINGERPRINT_SERVICE_NAME)) +- await this._maybeStartFingerprintVerification(); +- } +- +- _setFingerprintReaderType(fprintDeviceType) { +- this._fingerprintReaderType = +- FingerprintReaderType[fprintDeviceType.toUpperCase()]; +- +- if (this._fingerprintReaderType === undefined) +- throw new Error(`Unexpected fingerprint device type '${fprintDeviceType}'`); ++ this._maybeStartFingerprintVerification(); + } + + _onCredentialManagerAuthenticated(credentialManager, _token) { +@@ -702,7 +611,7 @@ var ShellUserVerifier = class { + } + + serviceIsFingerprint(serviceName) { +- return this._fingerprintReaderType !== FingerprintReaderType.NONE && ++ return this._fingerprintReaderFound && + serviceName === Constants.FINGERPRINT_SERVICE_NAME; + } + +@@ -715,10 +624,12 @@ var ShellUserVerifier = class { + let needsReset = false; + + if (this._settings.get_boolean(FINGERPRINT_AUTHENTICATION_KEY)) { +- this._initFingerprintManager().catch(logError); ++ this._initFingerprintManager(); + } else if (this._fingerprintManager) { ++ this._fingerprintManager.disconnectObject(this); + this._fingerprintManager = null; +- this._fingerprintReaderType = FingerprintReaderType.NONE; ++ this._fingerprintReaderFound = false; ++ this._fingerprintReaderType = FingerprintManager.FingerprintReaderType.NONE; + + if (this._activeServices.has(Constants.FINGERPRINT_SERVICE_NAME)) + needsReset = true; +@@ -746,7 +657,7 @@ var ShellUserVerifier = class { + return Constants.PASSWORD_SERVICE_NAME; + else if (this._smartcardManager) + return Constants.SMARTCARD_SERVICE_NAME; +- else if (this._fingerprintReaderType !== FingerprintReaderType.NONE) ++ else if (this._fingerprintReaderFound) + return Constants.FINGERPRINT_SERVICE_NAME; + return null; + } +@@ -802,7 +713,7 @@ var ShellUserVerifier = class { + + async _maybeStartFingerprintVerification() { + if (this._userName && +- this._fingerprintReaderType !== FingerprintReaderType.NONE && ++ this._fingerprintReaderFound && + !this.serviceIsForeground(Constants.FINGERPRINT_SERVICE_NAME)) + await this._startService(Constants.FINGERPRINT_SERVICE_NAME); + } +@@ -825,7 +736,7 @@ var ShellUserVerifier = class { + // 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 105076b40..1dba5f483 100644 +--- a/js/js-resources.gresource.xml ++++ b/js/js-resources.gresource.xml +@@ -6,6 +6,7 @@ + gdm/authPrompt.js + gdm/batch.js + gdm/constants.js ++ gdm/fingerprintManager.js + gdm/loginDialog.js + gdm/oVirt.js + gdm/credentialManager.js -- -2.51.0 +2.55.0 -From 17023fb0e31b0b18ef5cc47eeca2f08f38132ee7 Mon Sep 17 00:00:00 2001 +From 8808c34ad77e5c3af7c8f154d152c643905eb44b Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Tue, 23 Sep 2025 16:45:59 +0200 -Subject: [PATCH 38/48] misc: Add PasskeyDeviceManager +Subject: [PATCH 44/73] 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. ---- - js/js-resources.gresource.xml | 1 + - js/misc/passkeyDeviceManager.js | 69 +++++++++++++++++++++++++++++++++ - 2 files changed, 70 insertions(+) - create mode 100644 js/misc/passkeyDeviceManager.js -diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml -index ad4057d585..df6dbf5070 100644 ---- a/js/js-resources.gresource.xml -+++ b/js/js-resources.gresource.xml -@@ -29,6 +29,7 @@ - misc/objectManager.js - misc/params.js - misc/parentalControlsManager.js -+ misc/passkeyDeviceManager.js - misc/permissionStore.js - misc/smartcardManager.js - misc/systemActions.js -diff --git a/js/misc/passkeyDeviceManager.js b/js/misc/passkeyDeviceManager.js +Part-of: +--- + js/gdm/passkeyDeviceManager.js | 69 ++++++++++++++++++++++++++++++++++ + js/js-resources.gresource.xml | 1 + + 2 files changed, 70 insertions(+) + create mode 100644 js/gdm/passkeyDeviceManager.js + +diff --git a/js/gdm/passkeyDeviceManager.js b/js/gdm/passkeyDeviceManager.js new file mode 100644 -index 0000000000..20799975aa +index 000000000..20799975a --- /dev/null -+++ b/js/misc/passkeyDeviceManager.js ++++ b/js/gdm/passkeyDeviceManager.js @@ -0,0 +1,69 @@ +import GObject from 'gi://GObject'; +import GUdev from 'gi://GUdev'; @@ -4500,26 +4969,111 @@ index 0000000000..20799975aa + this.emit('passkey-removed', device); + } +} +diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml +index 1dba5f483..7ee877a6f 100644 +--- a/js/js-resources.gresource.xml ++++ b/js/js-resources.gresource.xml +@@ -11,6 +11,7 @@ + gdm/oVirt.js + gdm/credentialManager.js + gdm/vmware.js ++ gdm/passkeyDeviceManager.js + gdm/realmd.js + gdm/util.js + -- -2.51.0 +2.55.0 -From 9d32a080ea06fcdc7b653b7984d80ee7dfe62fcb Mon Sep 17 00:00:00 2001 +From 3e22b60badf7c1802d7f966e0bc74f1fe37f4962 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Tue, 19 May 2026 20:33:29 +0200 +Subject: [PATCH 45/73] 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 | 0 + js/gdm/util.js | 2 +- + js/js-resources.gresource.xml | 2 +- + js/ui/screenShield.js | 2 +- + 4 files changed, 3 insertions(+), 3 deletions(-) + rename js/{misc => gdm}/smartcardManager.js (100%) + +diff --git a/js/misc/smartcardManager.js b/js/gdm/smartcardManager.js +similarity index 100% +rename from js/misc/smartcardManager.js +rename to js/gdm/smartcardManager.js +diff --git a/js/gdm/util.js b/js/gdm/util.js +index d12969bac..e0f88ac7c 100644 +--- a/js/gdm/util.js ++++ b/js/gdm/util.js +@@ -13,7 +13,7 @@ const OVirt = imports.gdm.oVirt; + const Vmware = imports.gdm.vmware; + const Main = imports.ui.main; + const Params = imports.misc.params; +-const SmartcardManager = imports.misc.smartcardManager; ++const SmartcardManager = imports.gdm.smartcardManager; + + Gio._promisify(Gdm.Client.prototype, + 'open_reauthentication_channel', 'open_reauthentication_channel_finish'); +diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml +index 7ee877a6f..040b79612 100644 +--- a/js/js-resources.gresource.xml ++++ b/js/js-resources.gresource.xml +@@ -13,6 +13,7 @@ + gdm/vmware.js + gdm/passkeyDeviceManager.js + gdm/realmd.js ++ gdm/smartcardManager.js + gdm/util.js + + misc/config.js +@@ -31,7 +32,6 @@ + misc/params.js + misc/parentalControlsManager.js + misc/permissionStore.js +- misc/smartcardManager.js + misc/systemActions.js + misc/util.js + misc/weather.js +diff --git a/js/ui/screenShield.js b/js/ui/screenShield.js +index 436bc9408..9917f39d6 100644 +--- a/js/ui/screenShield.js ++++ b/js/ui/screenShield.js +@@ -12,7 +12,7 @@ const Main = imports.ui.main; + const Overview = imports.ui.overview; + const MessageTray = imports.ui.messageTray; + const ShellDBus = imports.ui.shellDBus; +-const SmartcardManager = imports.misc.smartcardManager; ++const SmartcardManager = imports.gdm.smartcardManager; + + const { adjustAnimationTime } = imports.ui.environment; + +-- +2.55.0 + + +From 2e9efd8f32e440f6debff1fbeb4c71ae94471d20 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Tue, 10 Mar 2026 14:27:50 +0100 -Subject: [PATCH 39/48] misc/smartcardManager: Skip login_token aliases to - avoid duplicate events +Subject: [PATCH 46/73] 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/misc/smartcardManager.js | 6 ++++++ + js/gdm/smartcardManager.js | 6 ++++++ 1 file changed, 6 insertions(+) -diff --git a/js/misc/smartcardManager.js b/js/misc/smartcardManager.js -index 26f9f5aaa9..cd95741b6c 100644 ---- a/js/misc/smartcardManager.js -+++ b/js/misc/smartcardManager.js +diff --git a/js/gdm/smartcardManager.js b/js/gdm/smartcardManager.js +index 26f9f5aaa..cd95741b6 100644 +--- a/js/gdm/smartcardManager.js ++++ b/js/gdm/smartcardManager.js @@ -66,6 +66,12 @@ var SmartcardManager = class { } @@ -4534,13 +5088,13 @@ index 26f9f5aaa9..cd95741b6c 100644 token.connect('g-properties-changed', (proxy, properties) => { -- -2.51.0 +2.55.0 -From fe23b7b14fa5c4c2f42e48603b6030e3e7e5f51d Mon Sep 17 00:00:00 2001 +From 2e0450029f3fb46af079b0788ee8e86ce831b065 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Mon, 18 Aug 2025 12:08:21 +0200 -Subject: [PATCH 40/48] gdm: Add AuthServices +Subject: [PATCH 47/73] gdm: Add AuthServices MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @@ -4609,41 +5163,77 @@ 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 | 39 +- - js/gdm/authServices.js | 468 +++++++++++++++++++++ - js/gdm/authServicesLegacy.js | 334 +++++++++++++++ - js/gdm/util.js | 742 ++++++++-------------------------- + js/gdm/authPrompt.js | 67 +-- + js/gdm/authServices.js | 498 +++++++++++++++++++++ + js/gdm/authServicesLegacy.js | 351 +++++++++++++++ + js/gdm/loginDialog.js | 2 +- + js/gdm/util.js | 819 ++++++++++------------------------ js/js-resources.gresource.xml | 2 + - po/POTFILES.in | 1 + - 6 files changed, 988 insertions(+), 598 deletions(-) + js/ui/unlockDialog.js | 2 +- + po/POTFILES.in | 2 + + 8 files changed, 1100 insertions(+), 643 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 a7ec1dcda4..5a92c725b5 100644 +index aa36fbb8b..4b58ed2e5 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -6,7 +6,6 @@ const { Clutter, Gio, GLib, GObject, Graphene, Meta, Pango, Shell, St } = import - const Animation = imports.ui.animation; - const AuthList = imports.gdm.authList; - const Batch = imports.gdm.batch; --const Constants = imports.gdm.constants; - const GdmUtil = imports.gdm.util; - const OVirt = imports.gdm.oVirt; - const Vmware = imports.gdm.vmware; -@@ -89,10 +88,7 @@ var 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), +@@ -48,7 +48,7 @@ var 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]}, +@@ -81,17 +81,14 @@ var AuthPrompt = GObject.registerClass({ + this._userVerifier = new GdmUtil.ShellUserVerifier(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)); -@@ -444,31 +440,6 @@ var AuthPrompt = GObject.registerClass({ +@@ -394,7 +391,7 @@ var AuthPrompt = GObject.registerClass({ + this._capsLockWarningLabel.visible = secret; + } + +- _onAskQuestion(serviceName, question, secret) { ++ _onAskQuestion({serviceName, question, secret}) { + if (this._queryingService) + this.clear(); + +@@ -422,7 +419,7 @@ var AuthPrompt = GObject.registerClass({ + this.emit('prompted'); + } + +- _onShowChoiceList(serviceName, promptMessage, choiceList) { ++ _onShowChoiceList({serviceName, promptMessage, choiceList}) { + if (this._queryingService) + this.clear(); + +@@ -437,32 +434,7 @@ var AuthPrompt = GObject.registerClass({ this.emit('prompted'); } @@ -4672,10 +5262,30 @@ index a7ec1dcda4..5a92c725b5 100644 - this.reset(); - } - - _onShowMessage(_userVerifier, serviceName, message, type) { - this.setMessage(serviceName, message, type); +- _onShowMessage(serviceName, message, type) { ++ _onShowMessage({serviceName, message, type}) { + let wiggleParameters = {duration: 0}; -@@ -813,8 +784,10 @@ var AuthPrompt = GObject.registerClass({ + if (type === GdmUtil.MessageType.ERROR && +@@ -490,13 +462,15 @@ var AuthPrompt = GObject.registerClass({ + } + } + +- _onVerificationFailed(serviceName, canRetry) { ++ _onVerificationFailed({serviceName, canRetry}) { + const wasQueryingService = this._queryingService === serviceName; + + if (wasQueryingService) + this._queryingService = null; + +- if (canRetry) { ++ // 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.startPreemptiveInput(); +@@ -794,8 +768,10 @@ var AuthPrompt = GObject.registerClass({ this._preemptiveAnswerWatchId = this._idleMonitor.add_idle_watch(500, this._onUserStoppedTypePreemptiveAnswer.bind(this)); @@ -4686,9 +5296,9 @@ index a7ec1dcda4..5a92c725b5 100644 + else + this._userVerifier?.reset(); - reuseEntryText = reuseEntryText || this._preemptiveInput; + reuseEntryText = reuseEntryText || !!this._preemptiveAnswer || this._preemptiveInput; -@@ -838,9 +811,7 @@ var AuthPrompt = GObject.registerClass({ +@@ -818,9 +794,7 @@ var AuthPrompt = GObject.registerClass({ if (oldStatus === AuthPromptStatus.VERIFICATION_CANCELLED) return; resetType = ResetType.PROVIDE_USERNAME; @@ -4699,18 +5309,27 @@ index a7ec1dcda4..5a92c725b5 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; +@@ -855,7 +829,8 @@ var 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 0000000000..d522ee352e +index 000000000..fd27583c2 --- /dev/null +++ b/js/gdm/authServices.js -@@ -0,0 +1,468 @@ -+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- -+ -+import * as FingerprintManager from '../misc/fingerprintManager.js'; +@@ -0,0 +1,498 @@ ++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 SmartcardManager from '../misc/smartcardManager.js'; ++import * as SmartcardManager from './smartcardManager.js'; +import {logErrorUnlessCancelled} from '../misc/errorUtils.js'; +import * as Util from './util.js'; +import Gdm from 'gi://Gdm'; @@ -4728,31 +5347,50 @@ index 0000000000..d522ee352e +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': {}, + }; + @@ -4788,8 +5426,12 @@ index 0000000000..d522ee352e + + this._cancellable = null; + -+ this._connectSmartcardManager(); -+ this._connectFingerprintManager(); ++ if (this.supportedRoles.includes(Constants.SMARTCARD_ROLE_NAME) && ++ this._enabledRoles.includes(Constants.SMARTCARD_ROLE_NAME)) ++ this._connectSmartcardManager(); ++ if (this.supportedRoles.includes(Constants.FINGERPRINT_ROLE_NAME) && ++ this._enabledRoles.includes(Constants.FINGERPRINT_ROLE_NAME)) ++ this._connectFingerprintManager(); + } + + get selectedMechanism() { @@ -4825,7 +5467,7 @@ index 0000000000..d522ee352e + 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++; @@ -4835,15 +5477,17 @@ index 0000000000..d522ee352e + 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(); + @@ -4878,6 +5522,7 @@ index 0000000000..d522ee352e + this._activeServices.clear(); + + this._verificationComplete = false; ++ this._userName = null; + + this._clearUserVerifier(); + @@ -4899,6 +5544,7 @@ index 0000000000..d522ee352e + } + + _updateEnabledMechanisms() { ++ this._selectedMechanism = null; + this._enabledMechanisms = []; + + this._handleUpdateEnabledMechanisms(); @@ -4944,7 +5590,7 @@ index 0000000000..d522ee352e + } + }); + -+ this.emit('wait-pending-messages', task); ++ this.emit('wait-pending-messages', {task}); + + return promise; + } @@ -5008,7 +5654,7 @@ index 0000000000..d522ee352e + _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); + } @@ -5017,10 +5663,11 @@ index 0000000000..d522ee352e + 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); @@ -5049,7 +5696,7 @@ index 0000000000..d522ee352e + + const doneTrying = !shouldRetry || !this._canRetry(); + -+ this.emit('verification-failed', serviceName, !doneTrying); ++ this.emit('verification-failed', {serviceName, canRetry: !doneTrying}); + + try { + await this._waitPendingMessages(); @@ -5121,7 +5768,10 @@ index 0000000000..d522ee352e + + _handleBeginVerification() {} + -+ _handleSelectMechanism() {} ++ _handleSelectMechanism() { ++ throw new GObject.NotImplementedError( ++ `_handleSelectMechanism in ${this.constructor.name}`); ++ } + + _handleNeedsUsername() { + return true; @@ -5175,15 +5825,15 @@ index 0000000000..d522ee352e +} diff --git a/js/gdm/authServicesLegacy.js b/js/gdm/authServicesLegacy.js new file mode 100644 -index 0000000000..e27107c85e +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'; + +import * as Constants from './constants.js'; -+import {FingerprintReaderType} from '../misc/fingerprintManager.js'; ++import {FingerprintReaderType} from './fingerprintManager.js'; +import {logErrorUnlessCancelled} from '../misc/errorUtils.js'; +import * as OVirt from './oVirt.js'; +import * as Util from './util.js'; @@ -5281,18 +5931,24 @@ index 0000000000..e27107c85e + + _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); + } + } + @@ -5314,22 +5970,27 @@ index 0000000000..e27107c85e + + _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, ++ }); + } + } + @@ -5337,10 +5998,11 @@ index 0000000000..e27107c85e + 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 && @@ -5376,7 +6038,7 @@ index 0000000000..e27107c85e + if (serviceName !== this._selectedMechanism?.serviceName) + return; + -+ this.emit('ask-question', serviceName, question, false); ++ this.emit('ask-question', {serviceName, question}); + } + + _handleOnSecretInfoQuery(serviceName, secretQuestion) { @@ -5392,7 +6054,11 @@ index 0000000000..e27107c85e + return; + } + -+ this.emit('ask-question', serviceName, secretQuestion, true); ++ this.emit('ask-question', { ++ serviceName, ++ question: secretQuestion, ++ secret: true, ++ }); + } + + _handleOnConversationStopped(serviceName) { @@ -5435,10 +6101,11 @@ index 0000000000..e27107c85e + !errorMessage) + return; + -+ this.emit('queue-message', ++ this.emit('queue-message', { + serviceName, -+ errorMessage, -+ Util.MessageType.ERROR); ++ message: errorMessage, ++ messageType: Util.MessageType.ERROR, ++ }); + } + + _handleVerificationFailed(serviceName) { @@ -5466,7 +6133,7 @@ index 0000000000..e27107c85e + 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() { @@ -5513,13 +6180,26 @@ index 0000000000..e27107c85e + this.emit('reset', {softReset: true}); + } +} +diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js +index 2d249852e..394d7f7e3 100644 +--- a/js/gdm/loginDialog.js ++++ b/js/gdm/loginDialog.js +@@ -906,7 +906,7 @@ var 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 1176f77500..2172e7b923 100644 +index e0f88ac7c..25728fdef 100644 --- a/js/gdm/util.js +++ b/js/gdm/util.js -@@ -2,29 +2,14 @@ - /* exported BANNER_MESSAGE_KEY, BANNER_MESSAGE_TEXT_KEY, LOGO_KEY, - DISABLE_USER_LIST_KEY, fadeInActor, fadeOutActor, cloneAndFadeOutActor */ +@@ -3,28 +3,14 @@ + DISABLE_USER_LIST_KEY, fadeInActor, fadeOutActor, cloneAndFadeOutActor, + isSelectable, getNonSelectableIconName */ -const { Clutter, Gdm, Gio, GLib } = imports.gi; +const { Clutter, Gio, GLib } = imports.gi; @@ -5527,13 +6207,12 @@ index 1176f77500..2172e7b923 100644 const Batch = imports.gdm.batch; const Constants = imports.gdm.constants; --const {FingerprintManager, FingerprintReaderType} = imports.misc.fingerprintManager; +-const FingerprintManager = imports.gdm.fingerprintManager; -const OVirt = imports.gdm.oVirt; -const Vmware = imports.gdm.vmware; const Main = imports.ui.main; --const { loadInterfaceXML } = imports.misc.fileUtils; const Params = imports.misc.params; --const SmartcardManager = imports.misc.smartcardManager; +-const SmartcardManager = imports.gdm.smartcardManager; - -Gio._promisify(Gdm.Client.prototype, - 'open_reauthentication_channel', 'open_reauthentication_channel_finish'); @@ -5545,21 +6224,29 @@ index 1176f77500..2172e7b923 100644 - 'call_begin_verification', 'call_begin_verification_finish'); -Gio._promisify(Gio.DBusProxy.prototype, - 'call', 'call_finish'); -+const { AuthServicesLegacy } = imports.gdm.authServicesLegacy; ++const {AuthServicesLegacy} = imports.gdm.authServicesLegacy; var FADE_ANIMATION_TIME = 160; var CLONE_FADE_ANIMATION_TIME = 250; -@@ -47,9 +32,6 @@ const MESSAGE_TIME_MULTIPLIER = (() => { +@@ -47,11 +33,13 @@ var MESSAGE_TIME_MULTIPLIER = (() => { return Number.isFinite(value) && value > 0 ? value : 1; })(); -const FINGERPRINT_SERVICE_PROXY_TIMEOUT = 5000; -const FINGERPRINT_ERROR_TIMEOUT_WAIT = 15; -- - var MessageType = { - // Keep messages in order by priority + +-var MessageType = { +- // Keep messages in order by priority ++/** ++ * Keep messages in order by priority ++ * ++ * @enum {number} ++ */ ++export const MessageType = { NONE: 0, -@@ -132,6 +114,20 @@ function cloneAndFadeOutActor(actor) { + HINT: 1, + INFO: 2, +@@ -104,6 +92,23 @@ function fadeOutActor(actor) { return hold; } @@ -5577,16 +6264,19 @@ index 1176f77500..2172e7b923 100644 + } +} + - /** - * @param {object} mechanism - * @returns {boolean} -@@ -174,41 +170,12 @@ var ShellUserVerifier = class { ++/** ++ * @param {Clutter.Actor} actor ++ */ + function cloneAndFadeOutActor(actor) { + // Immediately hide actor so its sibling can have its space + // and position, but leave a non-reactive clone on-screen, +@@ -174,41 +179,12 @@ var ShellUserVerifier = class { this._client = client; this._cancellable = null; - this._defaultService = null; - this._preemptingService = null; -- this._fingerprintReaderType = FingerprintReaderType.NONE; +- this._fingerprintReaderType = FingerprintManager.FingerprintReaderType.NONE; - this._fingerprintReaderFound = false; - this._messageQueue = []; @@ -5624,7 +6314,7 @@ index 1176f77500..2172e7b923 100644 } get hasPendingMessages() { -@@ -223,95 +190,85 @@ var ShellUserVerifier = class { +@@ -223,95 +199,100 @@ var ShellUserVerifier = class { return this._messageQueue ? this._messageQueue[0] : null; } @@ -5639,11 +6329,13 @@ index 1176f77500..2172e7b923 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, @@ -5655,9 +6347,10 @@ index 1176f77500..2172e7b923 100644 + hold?.release(); } - selectMechanism(mechanism) { +- selectMechanism() { - // TODO: Implement mechanism selection - return false; ++ selectMechanism(mechanism) { + return this._authServicesLegacy?.selectMechanism(mechanism); } @@ -5712,6 +6405,19 @@ index 1176f77500..2172e7b923 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; } @@ -5769,7 +6475,7 @@ index 1176f77500..2172e7b923 100644 } _getIntervalForMessage(message) { -@@ -322,7 +279,7 @@ var ShellUserVerifier = class { +@@ -322,7 +303,7 @@ var ShellUserVerifier = class { return message.length * USER_READ_TIME * MESSAGE_TIME_MULTIPLIER; } @@ -5778,7 +6484,35 @@ index 1176f77500..2172e7b923 100644 if (!this.hasPendingMessages) return; -@@ -365,7 +322,7 @@ var ShellUserVerifier = class { +@@ -340,12 +321,12 @@ var ShellUserVerifier = class { + 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}); + } + + _queueMessageTimeout() { +@@ -355,7 +336,11 @@ var ShellUserVerifier = class { + const message = this.currentMessage; + + delete this._currentMessageExtraInterval; +- this.emit('show-message', message.serviceName, message.text, message.type); ++ this.emit('show-message', { ++ serviceName: message.serviceName, ++ message: message.text, ++ type: message.type, ++ }); + + this._messageQueueTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, + message.interval + (this._currentMessageExtraInterval | 0), () => { +@@ -365,7 +350,7 @@ var ShellUserVerifier = class { this._messageQueue.shift(); this._queueMessageTimeout(); } else { @@ -5787,7 +6521,30 @@ index 1176f77500..2172e7b923 100644 } return GLib.SOURCE_REMOVE; -@@ -395,7 +352,7 @@ var ShellUserVerifier = class { +@@ -373,14 +358,14 @@ var ShellUserVerifier = class { + 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; +@@ -391,538 +376,182 @@ var ShellUserVerifier = class { + this._clearMessageQueue(); + + this._messageQueue = newQueue; +- this._queueMessage(serviceName, message, messageType); ++ this._queueMessage({serviceName, message, messageType}); } _clearMessageQueue() { @@ -5796,15 +6553,16 @@ index 1176f77500..2172e7b923 100644 if (this._messageQueueTimeoutId != 0) { GLib.source_remove(this._messageQueueTimeoutId); -@@ -404,459 +361,143 @@ var ShellUserVerifier = class { - this.emit('show-message', null, null, MessageType.NONE); - } - + this._messageQueueTimeoutId = 0; + } +- this.emit('show-message', null, null, MessageType.NONE); +- } +- - async _initFingerprintManager() { - if (this._fingerprintManager) - return; -- -- this._fingerprintManager = new FingerprintManager(this._cancellable); + +- this._fingerprintManager = FingerprintManager.getFingerprintManager(); - this._fingerprintManager.connectObject( - 'reader-type-changed', () => this._onFingerprintReaderTypeChanged(), - this); @@ -5824,20 +6582,22 @@ index 1176f77500..2172e7b923 100644 - // Ensure fingerprint service starts, but do not wait for it - this._fingerprintManager.checkReaderType(this._cancellable); - } -- } -+ _reportInitError(initError) { -+ const {cause, message, serviceName} = initError; ++ this.emit('show-message', {type: MessageType.NONE}); + } - _onFingerprintReaderTypeChanged() { - this._fingerprintReaderType = this._fingerprintManager.readerType; - this._fingerprintReaderFound = this._fingerprintManager.readerFound; - this._updateDefaultService(); -- ++ _reportInitError(initError) { ++ const {cause, message, serviceName} = initError; + - if (this._userVerifier && - !this._activeServices.has(Constants.FINGERPRINT_SERVICE_NAME)) - this._maybeStartFingerprintVerification(); - } -- ++ logError(cause, message); + - _onCredentialManagerAuthenticated(credentialManager, _token) { - this._preemptingService = credentialManager.service; - this.emit('credential-manager-authenticated'); @@ -5889,13 +6649,12 @@ index 1176f77500..2172e7b923 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(); @@ -5911,6 +6670,14 @@ index 1176f77500..2172e7b923 100644 - // verification from this login session - this._getUserVerifier(); - return; ++ this._queueMessage({ ++ serviceName, ++ message: _('Authentication error'), ++ messageType: MessageType.ERROR, ++ }); ++ this._verificationFailed({serviceName}); ++ } ++ + async _getUserVerifierProxies(userName, cancellable) { + const proxies = {}; + @@ -6025,7 +6792,16 @@ index 1176f77500..2172e7b923 100644 - } - - serviceIsForeground(serviceName) { -- return serviceName == this._getForegroundService(); +- return serviceName === this._getForegroundService(); +- } +- +- foregroundServiceDeterminesUsername() { +- for (let serviceName in this._credentialManagers) { +- if (this.serviceIsForeground(serviceName)) +- return true; +- } +- +- return this.serviceIsForeground(Constants.SMARTCARD_SERVICE_NAME); - } - - serviceIsDefault(serviceName) { @@ -6047,17 +6823,17 @@ index 1176f77500..2172e7b923 100644 - _updateEnabledServices() { - let needsReset = false; -+ _updateAuthServices() { -+ const enabledRoles = []; - +- - if (this._settings.get_boolean(FINGERPRINT_AUTHENTICATION_KEY)) { - this._initFingerprintManager(); - } else if (this._fingerprintManager) { - this._fingerprintManager.disconnectObject(this); - this._fingerprintManager = null; - this._fingerprintReaderFound = false; -- this._fingerprintReaderType = FingerprintReaderType.NONE; -- +- this._fingerprintReaderType = FingerprintManager.FingerprintReaderType.NONE; ++ _updateAuthServices() { ++ const enabledRoles = []; + - if (this._activeServices.has(Constants.FINGERPRINT_SERVICE_NAME)) - needsReset = true; - } @@ -6100,7 +6876,7 @@ index 1176f77500..2172e7b923 100644 - this._defaultService = this._getDetectedDefaultService(); - - if (!this._defaultService) { -- log("no authentication service is enabled, using password authentication"); +- log('no authentication service is enabled, using password authentication'); - this._defaultService = Constants.PASSWORD_SERVICE_NAME; - } - @@ -6139,26 +6915,26 @@ index 1176f77500..2172e7b923 100644 - } - this._hold?.release(); - } -- + - _beginVerification() { - this._startService(this._getForegroundService()); - this._maybeStartFingerprintVerification().catch(logError); - } - +- - async _maybeStartFingerprintVerification() { - if (this._userName && - this._fingerprintReaderFound && - !this.serviceIsForeground(Constants.FINGERPRINT_SERVICE_NAME)) - await this._startService(Constants.FINGERPRINT_SERVICE_NAME); - } -+ this._enabledRoles = enabledRoles; - +- - _onChoiceListQuery(client, serviceName, promptMessage, list) { - if (!this.serviceIsForeground(serviceName)) - return; -- ++ this._enabledRoles = enabledRoles; + - const choiceList = {}; -- for (const [key, value] of Object.entries(list.deepUnpack())) +- for (const [key, value] of Object.entries(list.deep_unpack())) - choiceList[key] = {title: value}; - - this.emit('show-choice-list', serviceName, promptMessage, choiceList); @@ -6171,7 +6947,7 @@ index 1176f77500..2172e7b923 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)'), @@ -6188,10 +6964,20 @@ index 1176f77500..2172e7b923 100644 - _onProblem(client, serviceName, problem) { - const isFingerprint = this.serviceIsFingerprint(serviceName); -- ++ _createAuthServices() { ++ this._clearAuthServices(); + - if (!this.serviceIsForeground(serviceName) && !isFingerprint) - return; -- ++ const params = { ++ client: this._client, ++ enabledRoles: this._enabledRoles, ++ allowedFailures: this.allowedFailures, ++ reauthOnly: this._reauthOnly, ++ }; ++ if (AuthServicesLegacy.supportsAny(this._enabledRoles)) ++ this._authServicesLegacy = new AuthServicesLegacy(params); + - this._queuePriorityMessage(serviceName, problem, MessageType.ERROR); - - if (isFingerprint) { @@ -6220,24 +7006,17 @@ index 1176f77500..2172e7b923 100644 - }); - } - } -- } -+ _createAuthServices() { -+ this._clearAuthServices(); ++ this._connectAuthServices(); + } - _onInfoQuery(client, serviceName, question) { - if (!this.serviceIsForeground(serviceName)) - return; -+ const params = { -+ client: this._client, -+ enabledRoles: this._enabledRoles, -+ allowedFailures: this.allowedFailures, -+ reauthOnly: this._reauthOnly, -+ }; -+ if (AuthServicesLegacy.supportsAny(this._enabledRoles)) -+ this._authServicesLegacy = new AuthServicesLegacy(params); - +- - this.emit('ask-question', serviceName, question, false); -+ this._connectAuthServices(); ++ _clearAuthServices() { ++ this._authServicesLegacy?.destroy(); ++ this._authServicesLegacy = null; } - _onSecretInfoQuery(client, serviceName, secretQuestion) { @@ -6254,9 +7033,23 @@ index 1176f77500..2172e7b923 100644 - } - - this.emit('ask-question', serviceName, secretQuestion, true); -+ _clearAuthServices() { -+ this._authServicesLegacy?.destroy(); -+ this._authServicesLegacy = null; ++ _connectAuthServices() { ++ [ ++ 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', () => 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); ++ }); } - _onReset() { @@ -6267,30 +7060,15 @@ index 1176f77500..2172e7b923 100644 - this._updateDefaultService(); - - this.emit('reset'); -+ _connectAuthServices() { -+ [ -+ 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), -+ this); -+ }); ++ _verificationFailed({serviceName, canRetry}) { ++ this._filterServiceMessages({serviceName, messageType: MessageType.ERROR}); ++ this.emit('verification-failed', {serviceName, canRetry}); } - _onVerificationComplete() { - this.emit('verification-complete'); -+ _verificationFailed(serviceName, canRetry) { -+ this._filterServiceMessages(serviceName, MessageType.ERROR); -+ this.emit('verification-failed', serviceName, canRetry); ++ get selectedMechanism() { ++ return this._authServicesLegacy?.selectedMechanism ?? null; } - _cancelAndReset() { @@ -6304,14 +7082,14 @@ index 1176f77500..2172e7b923 100644 - this._connectSignals(); - this._startService(serviceName); - } -+ const selectedMechanism = this._authServicesLegacy?.selectedMechanism ?? ++ const selectedMechanism = this.selectedMechanism ?? + mechanisms.find(m => isSelectable(m)) ?? + {}; - _canRetry() { - return this._userName && - (this._reauthOnly || this._failCounter < this.allowedFailures); -+ this.emit('mechanisms-changed', mechanisms, selectedMechanism); ++ this.emit('mechanisms-changed', {mechanisms, selectedMechanism}); } - _verificationFailed(serviceName, shouldRetry) { @@ -6342,19 +7120,39 @@ index 1176f77500..2172e7b923 100644 - this._cancelAndReset(); - }); - } -+ async _waitPendingMessages(task) { ++ async _waitPendingMessages({task}) { + try { + await this._handlePendingMessages(); + task.return_boolean(true); + } catch (e) { + task.return_error(e); } ++ } - this.emit('verification-failed', serviceName, !doneTrying); -@@ -873,47 +514,20 @@ var ShellUserVerifier = class { - } - } +- this.emit('verification-failed', serviceName, !doneTrying); ++ _handlePendingMessages() { ++ if (!this.hasPendingMessages) ++ return Promise.resolve(); +- if (!this.hasPendingMessages) { +- this._retry(serviceName); +- } else { +- const cancellable = this._cancellable; +- let signalId = this.connect('no-more-messages', () => { ++ const cancellable = this._cancellable; ++ return new Promise((resolve, reject) => { ++ const signalId = this.connect('no-more-messages', () => { + this.disconnect(signalId); +- if (!cancellable.is_cancelled()) +- this._retry(serviceName); ++ if (cancellable.is_cancelled()) ++ reject(new GLib.Error(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED, 'Operation was cancelled')); ++ else ++ resolve(); + }); +- } +- } +- - _onServiceUnavailable(_client, serviceName, errorMessage) { - this._unavailableServices.add(serviceName); - @@ -6394,26 +7192,14 @@ index 1176f77500..2172e7b923 100644 - // password authentication a chance to succeed - if (this.serviceIsForeground(serviceName)) - this._failCounter++; -+ _handlePendingMessages() { -+ if (!this.hasPendingMessages) -+ return Promise.resolve(); - +- - this._verificationFailed(serviceName, true); -+ const cancellable = this._cancellable; -+ return new Promise((resolve, reject) => { -+ const 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(); -+ }); + }); } }; Signals.addSignalMethods(ShellUserVerifier.prototype); diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml -index df6dbf5070..f2fb20c7a4 100644 +index 040b79612..e54793cd4 100644 --- a/js/js-resources.gresource.xml +++ b/js/js-resources.gresource.xml @@ -4,6 +4,8 @@ @@ -6424,27 +7210,41 @@ index df6dbf5070..f2fb20c7a4 100644 + gdm/authServicesLegacy.js gdm/batch.js gdm/constants.js - gdm/loginDialog.js + gdm/fingerprintManager.js +diff --git a/js/ui/unlockDialog.js b/js/ui/unlockDialog.js +index 5ecb7ed4b..14af2935f 100644 +--- a/js/ui/unlockDialog.js ++++ b/js/ui/unlockDialog.js +@@ -896,7 +896,7 @@ var 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 cb279c1ee5..a7a645a41c 100644 +index cb279c1ee..9dbd22c23 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in -@@ -6,6 +6,7 @@ data/org.gnome.shell.gschema.xml.in +@@ -6,6 +6,8 @@ data/org.gnome.shell.gschema.xml.in data/org.gnome.Shell.PortalHelper.desktop.in.in js/dbusServices/extensions/ui/extension-prefs-dialog.ui js/gdm/authPrompt.js ++js/gdm/authServices.js +js/gdm/authServicesLegacy.js js/gdm/loginDialog.js js/gdm/util.js js/misc/systemActions.js -- -2.51.0 +2.55.0 -From 86d48c601147b21a79d3f8c792f2ff4cfa320514 Mon Sep 17 00:00:00 2001 +From f94503642fe58d3cbad7f9300832a147946f6cda Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Mon, 16 Feb 2026 12:21:54 +0100 -Subject: [PATCH 41/48] gdm: Add fingerprint ready state to delay showing icon +Subject: [PATCH 48/73] 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 @@ -6457,16 +7257,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 d522ee352e..b3bfb56c6c 100644 +index fd27583c2..5abbb3e27 100644 --- a/js/gdm/authServices.js +++ b/js/gdm/authServices.js -@@ -90,7 +90,7 @@ export class AuthServices extends GObject.Object { +@@ -112,7 +112,7 @@ export class AuthServices extends GObject.Object { } get enabledMechanisms() { @@ -6476,7 +7278,7 @@ index d522ee352e..b3bfb56c6c 100644 get _roleToService() { diff --git a/js/gdm/authServicesLegacy.js b/js/gdm/authServicesLegacy.js -index e27107c85e..c0b9fdedd8 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'; @@ -6496,14 +7298,10 @@ index e27107c85e..c0b9fdedd8 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; @@ -6533,23 +7331,22 @@ index e27107c85e..c0b9fdedd8 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; } @@ -6559,23 +7356,20 @@ index e27107c85e..c0b9fdedd8 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.51.0 +2.55.0 -From 5ea67b48e5dc62cac9b08bc817c89a40e4b22ecd Mon Sep 17 00:00:00 2001 +From d03d20f3a9ef82a953e8258fb20be72fa657bc25 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Tue, 6 Feb 2024 14:09:34 -0500 -Subject: [PATCH 42/48] gdm: Add authServicesSSSDSwitchable +Subject: [PATCH 49/73] 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' @@ -6608,22 +7402,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(-) + 7 files changed, 212 insertions(+), 7 deletions(-) create mode 100644 js/gdm/authServicesSSSDSwitchable.js diff --git a/js/gdm/authServices.js b/js/gdm/authServices.js -index b3bfb56c6c..3416ce3055 100644 +index 5abbb3e27..67783e82a 100644 --- a/js/gdm/authServices.js +++ b/js/gdm/authServices.js -@@ -17,6 +17,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'); @@ -6633,10 +7429,10 @@ index b3bfb56c6c..3416ce3055 100644 static [GObject.signals] = { diff --git a/js/gdm/authServicesSSSDSwitchable.js b/js/gdm/authServicesSSSDSwitchable.js new file mode 100644 -index 0000000000..c67933b6f3 +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'; @@ -6657,10 +7453,6 @@ index 0000000000..c67933b6f3 + GObject.registerClass(this); + } + -+ constructor(params) { -+ super(params); -+ } -+ + _handleAnswerQuery(serviceName, answer) { + if (serviceName !== this._selectedMechanism?.serviceName) + return; @@ -6698,7 +7490,10 @@ index 0000000000..c67933b6f3 + this._selectedMechanism = null; + } + -+ _handleOnCustomJSONRequest(_serviceName, _protocol, _version, json) { ++ _handleOnCustomJSONRequest(serviceName, _protocol, _version, json) { ++ if (serviceName !== Constants.SWITCHABLE_AUTH_SERVICE_NAME) ++ return; ++ + let requestObject; + + try { @@ -6709,13 +7504,10 @@ index 0000000000..c67933b6f3 + } + + 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() { @@ -6732,7 +7524,7 @@ index 0000000000..c67933b6f3 + 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); + @@ -6740,16 +7532,22 @@ index 0000000000..c67933b6f3 + } + + _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, ++ }); + } + } + @@ -6800,14 +7598,14 @@ index 0000000000..c67933b6f3 + _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 ab085fae49..8e71e4f0ed 100644 +index 394d7f7e3..f5e439838 100644 --- a/js/gdm/loginDialog.js +++ b/js/gdm/loginDialog.js -@@ -326,7 +326,10 @@ var LoginDialog = GObject.registerClass({ +@@ -327,7 +327,10 @@ var LoginDialog = GObject.registerClass({ this._gdmClient = new Gdm.Client(); try { @@ -6820,18 +7618,18 @@ index ab085fae49..8e71e4f0ed 100644 } diff --git a/js/gdm/util.js b/js/gdm/util.js -index 2172e7b923..9d9722187d 100644 +index 25728fdef..7224376bf 100644 --- a/js/gdm/util.js +++ b/js/gdm/util.js -@@ -10,6 +10,7 @@ const Constants = imports.gdm.constants; +@@ -11,6 +11,7 @@ const Constants = imports.gdm.constants; const Main = imports.ui.main; const Params = imports.misc.params; - const { AuthServicesLegacy } = imports.gdm.authServicesLegacy; -+const { AuthServicesSSSDSwitchable } = imports.gdm.authServicesSSSDSwitchable; + const {AuthServicesLegacy} = imports.gdm.authServicesLegacy; ++const {AuthServicesSSSDSwitchable} = imports.gdm.authServicesSSSDSwitchable; var FADE_ANIMATION_TIME = 160; var CLONE_FADE_ANIMATION_TIME = 250; -@@ -18,6 +19,7 @@ var LOGIN_SCREEN_SCHEMA = 'org.gnome.login-screen'; +@@ -19,6 +20,7 @@ var LOGIN_SCREEN_SCHEMA = 'org.gnome.login-screen'; var PASSWORD_AUTHENTICATION_KEY = 'enable-password-authentication'; var FINGERPRINT_AUTHENTICATION_KEY = 'enable-fingerprint-authentication'; var SMARTCARD_AUTHENTICATION_KEY = 'enable-smartcard-authentication'; @@ -6839,15 +7637,15 @@ index 2172e7b923..9d9722187d 100644 var BANNER_MESSAGE_KEY = 'banner-message-enable'; var BANNER_MESSAGE_TEXT_KEY = 'banner-message-text'; var ALLOWED_FAILURES_KEY = 'allowed-failures'; -@@ -196,6 +198,7 @@ var ShellUserVerifier = class { - +@@ -206,6 +208,7 @@ var ShellUserVerifier = class { 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) { -@@ -207,14 +210,19 @@ var ShellUserVerifier = class { + if (e instanceof InitError) +@@ -218,14 +221,19 @@ var ShellUserVerifier = class { } selectMechanism(mechanism) { @@ -6869,7 +7667,7 @@ index 2172e7b923..9d9722187d 100644 this._authServicesLegacy?.reset(); this._userVerifier?.call_cancel_sync(null); -@@ -223,6 +231,7 @@ var ShellUserVerifier = class { +@@ -234,6 +242,7 @@ var ShellUserVerifier = class { } cancel() { @@ -6877,7 +7675,7 @@ index 2172e7b923..9d9722187d 100644 this._authServicesLegacy?.cancel(); this._userVerifier?.call_cancel_sync(null); -@@ -231,6 +240,7 @@ var ShellUserVerifier = class { +@@ -242,6 +251,7 @@ var ShellUserVerifier = class { } clear() { @@ -6885,7 +7683,7 @@ index 2172e7b923..9d9722187d 100644 this._authServicesLegacy?.clear(); this._clearMessageQueue(); -@@ -242,6 +252,9 @@ var ShellUserVerifier = class { +@@ -266,6 +276,9 @@ var ShellUserVerifier = class { } destroy() { @@ -6895,7 +7693,7 @@ index 2172e7b923..9d9722187d 100644 this._authServicesLegacy?.destroy(); this._authServicesLegacy = null; -@@ -252,6 +265,7 @@ var ShellUserVerifier = class { +@@ -276,6 +289,7 @@ var ShellUserVerifier = class { } selectChoice(serviceName, key) { @@ -6903,7 +7701,7 @@ index 2172e7b923..9d9722187d 100644 this._authServicesLegacy?.selectChoice(serviceName, key); } -@@ -260,6 +274,7 @@ var ShellUserVerifier = class { +@@ -284,6 +298,7 @@ var ShellUserVerifier = class { // ensure no messages get lost await this._handlePendingMessages().catch(logErrorUnlessCancelled); @@ -6911,7 +7709,7 @@ index 2172e7b923..9d9722187d 100644 this._authServicesLegacy?.answerQuery(serviceName, answer); } -@@ -430,10 +445,15 @@ var ShellUserVerifier = class { +@@ -463,10 +478,15 @@ var ShellUserVerifier = class { if (this._settings.get_boolean(FINGERPRINT_AUTHENTICATION_KEY)) enabledRoles.push(Constants.FINGERPRINT_ROLE_NAME); @@ -6928,7 +7726,7 @@ index 2172e7b923..9d9722187d 100644 this._createAuthServices(); } -@@ -447,19 +467,25 @@ var ShellUserVerifier = class { +@@ -480,19 +500,25 @@ var ShellUserVerifier = class { allowedFailures: this.allowedFailures, reauthOnly: this._reauthOnly, }; @@ -6955,7 +7753,14 @@ index 2172e7b923..9d9722187d 100644 this._authServicesLegacy, ].forEach(authServices => { authServices?.connectObject( -@@ -483,9 +509,13 @@ var ShellUserVerifier = class { +@@ -516,11 +542,15 @@ var ShellUserVerifier = class { + } + + get selectedMechanism() { +- return this._authServicesLegacy?.selectedMechanism ?? null; ++ return this._authServicesSSSDSwitchable?.selectedMechanism ?? ++ this._authServicesLegacy?.selectedMechanism ?? ++ null; } _onMechanismsChanged() { @@ -6964,15 +7769,10 @@ index 2172e7b923..9d9722187d 100644 + const mechanismsLegacy = this._authServicesLegacy?.enabledMechanisms ?? []; + const mechanisms = [...mechanismsSwitchable, ...mechanismsLegacy]; -- const selectedMechanism = this._authServicesLegacy?.selectedMechanism ?? -+ const selectedMechanism = -+ this._authServicesSSSDSwitchable?.selectedMechanism ?? -+ this._authServicesLegacy?.selectedMechanism ?? + const selectedMechanism = this.selectedMechanism ?? mechanisms.find(m => isSelectable(m)) ?? - {}; - diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml -index f2fb20c7a4..bde0cf66c4 100644 +index e54793cd4..056867891 100644 --- a/js/js-resources.gresource.xml +++ b/js/js-resources.gresource.xml @@ -6,6 +6,7 @@ @@ -6982,12 +7782,12 @@ index f2fb20c7a4..bde0cf66c4 100644 + gdm/authServicesSSSDSwitchable.js gdm/batch.js gdm/constants.js - gdm/loginDialog.js + gdm/fingerprintManager.js diff --git a/js/ui/unlockDialog.js b/js/ui/unlockDialog.js -index db6f1d4ba1..e4c63c92cb 100644 +index 14af2935f..da24bed05 100644 --- a/js/ui/unlockDialog.js +++ b/js/ui/unlockDialog.js -@@ -542,6 +542,7 @@ var UnlockDialog = GObject.registerClass({ +@@ -535,6 +535,7 @@ var UnlockDialog = GObject.registerClass({ try { this._gdmClient.set_enabled_extensions([ Gdm.UserVerifierChoiceList.interface_info().name, @@ -6996,25 +7796,25 @@ index db6f1d4ba1..e4c63c92cb 100644 } catch (e) { } diff --git a/po/POTFILES.in b/po/POTFILES.in -index a7a645a41c..f2592f04c6 100644 +index 9dbd22c23..d783d4f8e 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in -@@ -7,6 +7,7 @@ data/org.gnome.Shell.PortalHelper.desktop.in.in - js/dbusServices/extensions/ui/extension-prefs-dialog.ui +@@ -8,6 +8,7 @@ js/dbusServices/extensions/ui/extension-prefs-dialog.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/systemActions.js -- -2.51.0 +2.55.0 -From e3989e74e46b5ebddcb2b7dfbbbc3d50888cedab Mon Sep 17 00:00:00 2001 +From dfe17140fb528f9b16fd2cab34209b696f5b894b Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Wed, 12 Nov 2025 17:25:33 +0100 -Subject: [PATCH 43/48] authServicesSSSDSwitchable: Allow resetting expired +Subject: [PATCH 50/73] authServicesSSSDSwitchable: Allow resetting expired password JSON protocol can't inform when a password is expired, so it's needed to @@ -7024,20 +7824,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 | 26 +++++++++++++++++++++++++- - 1 file changed, 25 insertions(+), 1 deletion(-) + js/gdm/authServicesSSSDSwitchable.js | 29 ++++++++++++++++++++++++++++ + 1 file changed, 29 insertions(+) diff --git a/js/gdm/authServicesSSSDSwitchable.js b/js/gdm/authServicesSSSDSwitchable.js -index c67933b6f3..ecbd80dc81 100644 +index 04e97b076..e8b2e9620 100644 --- a/js/gdm/authServicesSSSDSwitchable.js +++ b/js/gdm/authServicesSSSDSwitchable.js -@@ -22,10 +22,18 @@ export class AuthServicesSSSDSwitchable extends AuthServices { - super(params); - } - -- _handleAnswerQuery(serviceName, answer) { -+ async _handleAnswerQuery(serviceName, answer) { +@@ -22,6 +22,14 @@ export class AuthServicesSSSDSwitchable extends AuthServices { if (serviceName !== this._selectedMechanism?.serviceName) return; @@ -7052,7 +7849,7 @@ index c67933b6f3..ecbd80dc81 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; @@ -7060,8 +7857,8 @@ index c67933b6f3..ecbd80dc81 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) { @@ -7072,31 +7869,36 @@ index c67933b6f3..ecbd80dc81 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.51.0 +2.55.0 -From 6f02e838291e52e0f3bf6a70e3c5d1d0ad208a6e Mon Sep 17 00:00:00 2001 +From b490d42694af85ef863108b717e352af243fec19 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Thu, 21 Aug 2025 22:25:02 +0200 -Subject: [PATCH 44/48] gdm: Allow starting authServicesLegacy as fallback +Subject: [PATCH 51/73] gdm: Allow starting authServicesLegacy as fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @@ -7125,18 +7927,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 | 65 +++++++++++++++++++++++++++- - js/gdm/util.js | 28 ++++++++++-- - 4 files changed, 114 insertions(+), 6 deletions(-) + js/gdm/authServices.js | 22 +++++++++ + 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 3416ce3055..cfc16b9d7f 100644 +index 67783e82a..28a07558d 100644 --- a/js/gdm/authServices.js +++ b/js/gdm/authServices.js -@@ -102,6 +102,10 @@ export class AuthServices extends GObject.Object { +@@ -124,6 +124,10 @@ export class AuthServices extends GObject.Object { return this.constructor.SupportedRoles; } @@ -7147,7 +7951,7 @@ index 3416ce3055..cfc16b9d7f 100644 selectChoice(serviceName, key) { this._handleSelectChoice(serviceName, key); } -@@ -178,6 +182,18 @@ export class AuthServices extends GObject.Object { +@@ -203,6 +207,18 @@ export class AuthServices extends GObject.Object { this._handleClear(); } @@ -7166,7 +7970,7 @@ index 3416ce3055..cfc16b9d7f 100644 _clearUserVerifier() { this._disconnectUserVerifierSignals(); this._userVerifier = null; -@@ -409,6 +425,10 @@ export class AuthServices extends GObject.Object { +@@ -436,6 +452,10 @@ export class AuthServices extends GObject.Object { } } @@ -7177,7 +7981,7 @@ index 3416ce3055..cfc16b9d7f 100644 _handleSelectChoice() {} _handleAnswerQuery() {} -@@ -427,6 +447,8 @@ export class AuthServices extends GObject.Object { +@@ -457,6 +477,8 @@ export class AuthServices extends GObject.Object { _handleClear() {} @@ -7187,11 +7991,11 @@ index 3416ce3055..cfc16b9d7f 100644 throw new GObject.NotImplementedError( `_handleUpdateEnabledMechanisms in ${this.constructor.name}`); diff --git a/js/gdm/authServicesLegacy.js b/js/gdm/authServicesLegacy.js -index c0b9fdedd8..c1ff1242dc 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() { @@ -7200,10 +8004,10 @@ index c0b9fdedd8..c1ff1242dc 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 ecbd80dc81..a33ee0df91 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'; @@ -7219,22 +8023,28 @@ index ecbd80dc81..a33ee0df91 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); } - async _handleAnswerQuery(serviceName, answer) { -@@ -51,13 +59,30 @@ export class AuthServicesSSSDSwitchable extends AuthServices { ++ constructor(params) { ++ super(params); ++ ++ this._mechanismsStatus = MechanismsStatus.WAITING; ++ } ++ + _handleAnswerQuery(serviceName, answer) { + if (serviceName !== this._selectedMechanism?.serviceName) + return; +@@ -47,13 +59,32 @@ export class AuthServicesSSSDSwitchable extends AuthServices { } } + _handleGetUnsupportedRoles() { -+ // Until we know the mechanisms or wait for them, -+ // consider supportedRoles as supported ++ // 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: @@ -7260,28 +8070,27 @@ index ecbd80dc81..a33ee0df91 100644 } _handleClear() { -@@ -72,6 +97,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) -+ log(`Received unexpected JSON message, something might be wrong`); ++ log('Received unexpected JSON message, something might be wrong'); + try { requestObject = JSON.parse(json); } catch (e) { -@@ -87,6 +115,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 +143,9 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + this._updateEnabledMechanisms(); + } +@@ -107,6 +144,9 @@ export class AuthServicesSSSDSwitchable extends AuthServices { } _handleOnInfo(serviceName, info) { @@ -7291,7 +8100,7 @@ index ecbd80dc81..a33ee0df91 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 +158,9 @@ export class AuthServicesSSSDSwitchable extends AuthServices { +@@ -124,6 +164,9 @@ export class AuthServicesSSSDSwitchable extends AuthServices { } _handleOnProblem(serviceName, problem) { @@ -7299,9 +8108,9 @@ index ecbd80dc81..a33ee0df91 100644 + return; + if (serviceName === this._selectedMechanism?.serviceName) { - this.emit('queue-priority-message', + this.emit('queue-priority-message', { serviceName, -@@ -131,7 +169,16 @@ export class AuthServicesSSSDSwitchable extends AuthServices { +@@ -133,7 +176,16 @@ export class AuthServicesSSSDSwitchable extends AuthServices { } } @@ -7317,8 +8126,8 @@ index ecbd80dc81..a33ee0df91 100644 + if (serviceName === this._selectedMechanism?.serviceName && this._selectedMechanism.role === Constants.PASSWORD_ROLE_NAME && - this._resettingPassword) -@@ -151,7 +198,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 && @@ -7327,7 +8136,7 @@ index ecbd80dc81..a33ee0df91 100644 } _formatResponse(answer) { -@@ -182,6 +229,20 @@ export class AuthServicesSSSDSwitchable extends AuthServices { +@@ -189,6 +241,20 @@ export class AuthServicesSSSDSwitchable extends AuthServices { serviceName, JSON.stringify(response), this._cancellable).catch(logErrorUnlessCancelled); } @@ -7349,10 +8158,10 @@ index ecbd80dc81..a33ee0df91 100644 const {serviceName, prompt} = this._selectedMechanism; diff --git a/js/gdm/util.js b/js/gdm/util.js -index 9d9722187d..e69761747b 100644 +index 7224376bf..caf28e458 100644 --- a/js/gdm/util.js +++ b/js/gdm/util.js -@@ -243,6 +243,11 @@ var ShellUserVerifier = class { +@@ -254,6 +254,11 @@ var ShellUserVerifier = class { this._authServicesSSSDSwitchable?.clear(); this._authServicesLegacy?.clear(); @@ -7364,7 +8173,7 @@ index 9d9722187d..e69761747b 100644 this._clearMessageQueue(); this._cancellable?.cancel(); -@@ -468,10 +473,14 @@ var ShellUserVerifier = class { +@@ -501,10 +506,14 @@ var ShellUserVerifier = class { reauthOnly: this._reauthOnly, }; if (this._switchableAuthenticationEnabled && @@ -7381,7 +8190,7 @@ index 9d9722187d..e69761747b 100644 this._connectAuthServices(); } -@@ -509,9 +518,12 @@ var ShellUserVerifier = class { +@@ -548,9 +557,12 @@ var ShellUserVerifier = class { } _onMechanismsChanged() { @@ -7394,10 +8203,10 @@ index 9d9722187d..e69761747b 100644 - const mechanisms = [...mechanismsSwitchable, ...mechanismsLegacy]; + const mechanisms = [...mechanismsSSSDSwitchable, ...mechanismsLegacy]; - const selectedMechanism = - this._authServicesSSSDSwitchable?.selectedMechanism ?? -@@ -522,6 +534,14 @@ var ShellUserVerifier = class { - this.emit('mechanisms-changed', mechanisms, selectedMechanism); + const selectedMechanism = this.selectedMechanism ?? + mechanisms.find(m => isSelectable(m)) ?? +@@ -559,6 +571,14 @@ var ShellUserVerifier = class { + this.emit('mechanisms-changed', {mechanisms, selectedMechanism}); } + _enableFallbackMechanisms() { @@ -7408,17 +8217,17 @@ index 9d9722187d..e69761747b 100644 + this._authServicesSSSDSwitchable.unsupportedRoles); + } + - async _waitPendingMessages(task) { + async _waitPendingMessages({task}) { try { await this._handlePendingMessages(); -- -2.51.0 +2.55.0 -From 124b735692aa5f8f892dc739c15c047a2ae64c1d Mon Sep 17 00:00:00 2001 +From 6602b41ff3e32e5c6c71bbed7756aaa0af47c428 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 45/48] ui/qrCode: Add a QR Code widget +Subject: [PATCH 52/73] 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 @@ -7431,18 +8240,30 @@ 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 +++ data/theme/meson.build | 1 + js/js-resources.gresource.xml | 1 + js/ui/qrCode.js | 118 ++++++++++++++++++ - 4 files changed, 134 insertions(+) + 5 files changed, 135 insertions(+) create mode 100644 data/theme/gnome-shell-sass/widgets/_qr-code.scss create mode 100644 js/ui/qrCode.js +diff --git a/data/theme/gnome-shell-sass/_widgets.scss b/data/theme/gnome-shell-sass/_widgets.scss +index a8d0aa9f4..ebd97be3b 100644 +--- a/data/theme/gnome-shell-sass/_widgets.scss ++++ b/data/theme/gnome-shell-sass/_widgets.scss +@@ -49,3 +49,4 @@ + // Lock / login screens + @import 'widgets/login-dialog'; + @import 'widgets/screen-shield'; ++@import 'widgets/qr-code'; diff --git a/data/theme/gnome-shell-sass/widgets/_qr-code.scss b/data/theme/gnome-shell-sass/widgets/_qr-code.scss new file mode 100644 -index 0000000000..da04425bd6 +index 000000000..da04425bd --- /dev/null +++ b/data/theme/gnome-shell-sass/widgets/_qr-code.scss @@ -0,0 +1,14 @@ @@ -7461,19 +8282,19 @@ index 0000000000..da04425bd6 + } +} diff --git a/data/theme/meson.build b/data/theme/meson.build -index 87112c358d..169262d518 100644 +index 87112c358..a425a52e8 100644 --- a/data/theme/meson.build +++ b/data/theme/meson.build -@@ -30,6 +30,7 @@ theme_sources = files([ +@@ -29,6 +29,7 @@ theme_sources = files([ + 'gnome-shell-sass/widgets/_overview.scss', 'gnome-shell-sass/widgets/_panel.scss', 'gnome-shell-sass/widgets/_popovers.scss', - 'gnome-shell-sass/widgets/_screen-shield.scss', + 'gnome-shell-sass/widgets/_qr-code.scss', + 'gnome-shell-sass/widgets/_screen-shield.scss', 'gnome-shell-sass/widgets/_scrollbars.scss', 'gnome-shell-sass/widgets/_search-entry.scss', - 'gnome-shell-sass/widgets/_search-results.scss', diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml -index bde0cf66c4..e8575a7b6e 100644 +index 056867891..890c85ca3 100644 --- a/js/js-resources.gresource.xml +++ b/js/js-resources.gresource.xml @@ -95,6 +95,7 @@ @@ -7486,7 +8307,7 @@ index bde0cf66c4..e8575a7b6e 100644 ui/runDialog.js diff --git a/js/ui/qrCode.js b/js/ui/qrCode.js new file mode 100644 -index 0000000000..caf0999f34 +index 000000000..caf0999f3 --- /dev/null +++ b/js/ui/qrCode.js @@ -0,0 +1,118 @@ @@ -7609,13 +8430,151 @@ index 0000000000..caf0999f34 + } +}; -- -2.51.0 +2.55.0 -From 634b290c79fbc7f552b536dbb121e25ea435df41 Mon Sep 17 00:00:00 2001 +From e4b16979f857abdd84a2b0cd3e422f4cd76f7b4b 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/73] 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: +--- + js/gdm/authPrompt.js | 38 +++++++++++++++++++++++++++++++++++++- + js/gdm/authServices.js | 6 ++++++ + js/gdm/util.js | 1 + + 3 files changed, 44 insertions(+), 1 deletion(-) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index 4b58ed2e5..3825b5280 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -84,6 +84,7 @@ var 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(), +@@ -282,6 +283,16 @@ var 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({ +@@ -456,12 +467,34 @@ var 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}); + } + } + ++ _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; + +@@ -583,6 +616,7 @@ var AuthPrompt = GObject.registerClass({ + this._authListTitle.child.text = ''; + this._authList.clear(); + this._authList.hide(); ++ this._authButton.hide(); + + this._mainBox.opacity = 255; + this._mainBox.reactive = true; +@@ -597,6 +631,7 @@ var AuthPrompt = GObject.registerClass({ + this._entry.hint_text = question; + + this._authList.hide(); ++ this._authButton.hide(); + + this._fadeInElement(this._entryArea); + this.updateSensitivity({sensitive: true}); +@@ -682,6 +717,7 @@ var 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 caf28e458..6c69e927c 100644 +--- a/js/gdm/util.js ++++ b/js/gdm/util.js +@@ -540,6 +540,7 @@ var ShellUserVerifier = class { + '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); + }); +-- +2.55.0 + + +From 16b7f492f07675ebd5644f638fcc22773c08f679 Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Tue, 6 Feb 2024 14:18:24 -0500 -Subject: [PATCH 46/48] gdm: Add support for Web Login in +Subject: [PATCH 54/73] gdm: Add support for Web Login in authServicesSSSDSwitchable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 @@ -7630,26 +8589,25 @@ 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: --- - js/gdm/authPrompt.js | 105 ++++++++- - js/gdm/authServices.js | 6 + - js/gdm/authServicesSSSDSwitchable.js | 80 +++++++ + js/gdm/authPrompt.js | 82 +++++++- + js/gdm/authServices.js | 8 + + js/gdm/authServicesSSSDSwitchable.js | 96 +++++++++ js/gdm/constants.js | 1 + - js/gdm/loginDialog.js | 5 +- js/gdm/util.js | 5 + - js/gdm/webLogin.js | 308 +++++++++++++++++++++++++++ + js/gdm/webLogin.js | 285 +++++++++++++++++++++++++++ js/js-resources.gresource.xml | 1 + - js/ui/unlockDialog.js | 3 +- js/ui/userWidget.js | 8 + po/POTFILES.in | 1 + - 11 files changed, 511 insertions(+), 12 deletions(-) + 9 files changed, 479 insertions(+), 8 deletions(-) create mode 100644 js/gdm/webLogin.js diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index 5a92c725b5..8c5c8a1fab 100644 +index 3825b5280..a00feed40 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -12,6 +12,7 @@ const Vmware = imports.gdm.vmware; +@@ -13,6 +13,7 @@ const Vmware = imports.gdm.vmware; const Params = imports.misc.params; const ShellEntry = imports.ui.shellEntry; const UserWidget = imports.ui.userWidget; @@ -7657,48 +8615,48 @@ index 5a92c725b5..8c5c8a1fab 100644 const Util = imports.misc.util; var DEFAULT_BUTTON_WELL_ICON_SIZE = 16; -@@ -85,6 +86,7 @@ var 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), -@@ -297,6 +299,28 @@ var AuthPrompt = GObject.registerClass({ - this._defaultButtonWell.add_child(this._spinner); +@@ -86,6 +87,7 @@ var 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), +@@ -293,6 +295,18 @@ var AuthPrompt = GObject.registerClass({ + }); + this._mainBox.add_child(this._authButton); - this.setActorInDefaultButtonWell(this._nextButton); -+ -+ this._webLoginIntro = new WebLogin.WebLoginIntro(); -+ this._webLoginIntro.set({ -+ 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); - } - - _updateShowPasswordIcon() { -@@ -440,6 +464,52 @@ var AuthPrompt = GObject.registerClass({ ++ this.add_child(this._webLoginDialog); ++ + // center elements inside _mainBox between the cancel + // button on the left and this spacer on the right + this._mainBox.add_child(new Clutter.Actor({ +@@ -468,7 +482,8 @@ var AuthPrompt = GObject.registerClass({ + type < GdmUtil.MessageType.ERROR && + !this._entryArea.visible && + !this._authList.visible && +- !this._authButton.visible) { ++ !this._authButton.visible && ++ !this._webLoginDialog.visible) { + this._fadeInElement(this._entryArea); + this.updateSensitivity({sensitive: true}); + } +@@ -495,6 +510,47 @@ var 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(); + @@ -7710,15 +8668,9 @@ index 5a92c725b5..8c5c8a1fab 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'); + } @@ -7739,38 +8691,26 @@ index 5a92c725b5..8c5c8a1fab 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'); + } + - _onShowMessage(_userVerifier, serviceName, message, type) { - this.setMessage(serviceName, message, type); + _onVerificationFailed({serviceName, canRetry}) { + const wasQueryingService = this._queryingService === serviceName; -@@ -448,7 +518,9 @@ var AuthPrompt = GObject.registerClass({ - if (message && - type < GdmUtil.MessageType.ERROR && - !this._entryArea.visible && -- !this._authList.visible) -+ !this._authList.visible && -+ !this._webLoginIntro.visible && -+ !this._webLoginDialog.visible) - this._fadeInElement(this._entryArea); - - this.emit('prompted'); -@@ -474,12 +546,14 @@ var AuthPrompt = GObject.registerClass({ +@@ -521,11 +577,13 @@ var 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, @@ -7779,7 +8719,7 @@ index 5a92c725b5..8c5c8a1fab 100644 }); this.emit('verification-complete'); -@@ -551,6 +625,9 @@ var AuthPrompt = GObject.registerClass({ +@@ -597,6 +655,9 @@ var AuthPrompt = GObject.registerClass({ stopSpinning({animate = false} = {}) { this.emit('loading', false); this.setActorInDefaultButtonWell(this._nextButton, animate); @@ -7789,61 +8729,58 @@ index 5a92c725b5..8c5c8a1fab 100644 } clear(params) { -@@ -568,10 +645,14 @@ var AuthPrompt = GObject.registerClass({ - this._authListTitle.child.text = ''; +@@ -617,9 +678,12 @@ var 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) { -@@ -583,6 +664,8 @@ var AuthPrompt = GObject.registerClass({ - this._entry.hint_text = question; +@@ -632,6 +696,7 @@ var AuthPrompt = GObject.registerClass({ this._authList.hide(); -+ this._webLoginIntro.hide(); + this._authButton.hide(); + this._closeWebLoginDialog(); this._fadeInElement(this._entryArea); - } -@@ -692,6 +775,8 @@ var AuthPrompt = GObject.registerClass({ - updateSensitivity({sensitive}) { + this.updateSensitivity({sensitive: true}); +@@ -718,6 +783,7 @@ var 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 cfc16b9d7f..a62a3de901 100644 +index fc7861de6..b5a8d870f 100644 --- a/js/gdm/authServices.js +++ b/js/gdm/authServices.js -@@ -48,6 +48,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 a33ee0df91..5369334b0a 100644 +index 2a08be1f0..a321f4489 100644 --- a/js/gdm/authServicesSSSDSwitchable.js +++ b/js/gdm/authServicesSSSDSwitchable.js @@ -1,3 +1,4 @@ @@ -7874,7 +8811,7 @@ index a33ee0df91..5369334b0a 100644 } } -@@ -92,6 +98,8 @@ export class AuthServicesSSSDSwitchable extends AuthServices { +@@ -94,6 +100,8 @@ export class AuthServicesSSSDSwitchable extends AuthServices { this._selectedMechanism = null; this._resettingPassword = false; @@ -7882,8 +8819,8 @@ index a33ee0df91..5369334b0a 100644 + this._clearWebLoginTimeout(); } - _handleOnCustomJSONRequest(_serviceName, _protocol, _version, json) { -@@ -131,6 +139,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))); @@ -7892,7 +8829,7 @@ index a33ee0df91..5369334b0a 100644 const selectedMechanism = this._enabledMechanisms .find(m => this._savedMechanism?.role === m.role) ?? -@@ -142,6 +152,29 @@ export class AuthServicesSSSDSwitchable extends AuthServices { +@@ -143,6 +153,29 @@ export class AuthServicesSSSDSwitchable extends AuthServices { this._savedMechanism = null; } @@ -7922,7 +8859,7 @@ index a33ee0df91..5369334b0a 100644 _handleOnInfo(serviceName, info) { if (!this._eventExpected()) return; -@@ -210,6 +243,10 @@ export class AuthServicesSSSDSwitchable extends AuthServices { +@@ -222,6 +255,10 @@ export class AuthServicesSSSDSwitchable extends AuthServices { response = {password: answer}; break; } @@ -7933,9 +8870,9 @@ index a33ee0df91..5369334b0a 100644 default: throw new GObject.NotImplementedError(`formatResponse: ${role}`); } -@@ -248,4 +285,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() { @@ -7960,7 +8897,23 @@ index a33ee0df91..5369334b0a 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() { @@ -7982,10 +8935,10 @@ index a33ee0df91..5369334b0a 100644 + } } diff --git a/js/gdm/constants.js b/js/gdm/constants.js -index 2f37446c8a..e8ca48625a 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'; @@ -7993,27 +8946,11 @@ index 2f37446c8a..e8ca48625a 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 8e71e4f0ed..f59663401b 100644 ---- a/js/gdm/loginDialog.js -+++ b/js/gdm/loginDialog.js -@@ -611,7 +611,10 @@ var LoginDialog = GObject.registerClass({ - let authPromptAllocation = null; - let authPromptWidth = 0; - if (this._authPrompt.visible) { -- authPromptAllocation = this._getFixedTopActorAllocation(dialogBox, this._authPrompt); -+ if (this._authPrompt.webLoginActive) -+ authPromptAllocation = this._getCenterActorAllocation(dialogBox, this._authPrompt); -+ else -+ authPromptAllocation = this._getFixedTopActorAllocation(dialogBox, this._authPrompt); - authPromptWidth = authPromptAllocation.x2 - authPromptAllocation.x1; - } - diff --git a/js/gdm/util.js b/js/gdm/util.js -index e69761747b..6c160d323b 100644 +index 6c69e927c..36ff70427 100644 --- a/js/gdm/util.js +++ b/js/gdm/util.js -@@ -20,6 +20,7 @@ var PASSWORD_AUTHENTICATION_KEY = 'enable-password-authentication'; +@@ -21,6 +21,7 @@ var PASSWORD_AUTHENTICATION_KEY = 'enable-password-authentication'; var FINGERPRINT_AUTHENTICATION_KEY = 'enable-fingerprint-authentication'; var SMARTCARD_AUTHENTICATION_KEY = 'enable-smartcard-authentication'; var SWITCHABLE_AUTHENTICATION_KEY = 'enable-switchable-authentication'; @@ -8021,7 +8958,7 @@ index e69761747b..6c160d323b 100644 var BANNER_MESSAGE_KEY = 'banner-message-enable'; var BANNER_MESSAGE_TEXT_KEY = 'banner-message-text'; var ALLOWED_FAILURES_KEY = 'allowed-failures'; -@@ -138,6 +139,7 @@ export function isSelectable(mechanism) { +@@ -147,6 +148,7 @@ function isSelectable(mechanism) { switch (mechanism.role) { case Constants.PASSWORD_ROLE_NAME: case Constants.SMARTCARD_ROLE_NAME: @@ -8029,7 +8966,7 @@ index e69761747b..6c160d323b 100644 return true; case Constants.FINGERPRINT_ROLE_NAME: return false; -@@ -449,6 +451,8 @@ var ShellUserVerifier = class { +@@ -482,6 +484,8 @@ var ShellUserVerifier = class { enabledRoles.push(Constants.SMARTCARD_ROLE_NAME); if (this._settings.get_boolean(FINGERPRINT_AUTHENTICATION_KEY)) enabledRoles.push(Constants.FINGERPRINT_ROLE_NAME); @@ -8038,24 +8975,21 @@ index e69761747b..6c160d323b 100644 const switchableAuthentication = this._settings.get_boolean(SWITCHABLE_AUTHENTICATION_KEY); -@@ -508,6 +512,7 @@ var ShellUserVerifier = class { - '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), +@@ -542,6 +546,7 @@ var ShellUserVerifier = class { + '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 0000000000..8f2a0d8767 +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'; @@ -8069,8 +9003,11 @@ index 0000000000..8f2a0d8767 +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, @@ -8176,14 +9113,18 @@ index 0000000000..8f2a0d8767 + + 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, @@ -8250,9 +9191,10 @@ index 0000000000..8f2a0d8767 + 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', + }), + }); + @@ -8331,63 +9273,21 @@ index 0000000000..8f2a0d8767 + 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 e8575a7b6e..40702ccabc 100644 +index 890c85ca3..beee88568 100644 --- a/js/js-resources.gresource.xml +++ b/js/js-resources.gresource.xml -@@ -15,6 +15,7 @@ - gdm/vmware.js +@@ -18,6 +18,7 @@ gdm/realmd.js + gdm/smartcardManager.js gdm/util.js + gdm/webLogin.js misc/config.js misc/extensionUtils.js -diff --git a/js/ui/unlockDialog.js b/js/ui/unlockDialog.js -index e4c63c92cb..bebc7ad40a 100644 ---- a/js/ui/unlockDialog.js -+++ b/js/ui/unlockDialog.js -@@ -466,7 +466,8 @@ class UnlockDialogLayout extends Clutter.LayoutManager { - // Authentication Box - const dialog = container.get_parent(); - let stackY; -- if (dialog._activePage === dialog._clock) { -+ if (dialog._activePage === dialog._clock || -+ dialog._authPrompt?.webLoginActive) { - stackY = Math.min( - Math.floor(centerY - stackHeight / 2.0), - height - stackHeight - maxNotificationsHeight); diff --git a/js/ui/userWidget.js b/js/ui/userWidget.js -index c376028af3..80bf795239 100644 +index c376028af..80bf79523 100644 --- a/js/ui/userWidget.js +++ b/js/ui/userWidget.js @@ -247,4 +247,12 @@ class UserWidget extends St.BoxLayout { @@ -8404,10 +9304,10 @@ index c376028af3..80bf795239 100644 + } }); diff --git a/po/POTFILES.in b/po/POTFILES.in -index f2592f04c6..dfcd19cf0e 100644 +index d783d4f8e..5786f7436 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in -@@ -10,6 +10,7 @@ js/gdm/authServicesLegacy.js +@@ -11,6 +11,7 @@ js/gdm/authServicesLegacy.js js/gdm/authServicesSSSDSwitchable.js js/gdm/loginDialog.js js/gdm/util.js @@ -8416,13 +9316,13 @@ index f2592f04c6..dfcd19cf0e 100644 js/misc/util.js js/portalHelper/main.js -- -2.51.0 +2.55.0 -From d77e9e49c2e8c0e5f03cddd2c30ad68041b74bd7 Mon Sep 17 00:00:00 2001 +From bcafae2705e5bfd89884f529ce41b703f628d3ce Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Tue, 14 Jan 2025 07:31:59 -0500 -Subject: [PATCH 47/48] gdm: Add support for Smartcard in +Subject: [PATCH 55/73] gdm: Add support for Smartcard in authServicesSSSDSwitchable This allows selecting smartcard as a login method. @@ -8434,12 +9334,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 5369334b0a..e04814caf3 100644 +index a321f4489..668637db0 100644 --- a/js/gdm/authServicesSSSDSwitchable.js +++ b/js/gdm/authServicesSSSDSwitchable.js @@ -15,11 +15,13 @@ const MechanismsStatus = { @@ -8456,7 +9358,7 @@ index 5369334b0a..e04814caf3 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; } @@ -8468,14 +9370,18 @@ index 5369334b0a..e04814caf3 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, ++ }); + } + } + - async _handleAnswerQuery(serviceName, answer) { + _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: @@ -8483,7 +9389,7 @@ index 5369334b0a..e04814caf3 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; @@ -8493,7 +9399,7 @@ index 5369334b0a..e04814caf3 100644 case Constants.WEB_LOGIN_ROLE_NAME: this._startWebLogin(); break; -@@ -96,6 +114,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { +@@ -98,6 +120,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { this._priorityList = null; this._enabledMechanisms = null; this._selectedMechanism = null; @@ -8501,7 +9407,7 @@ index 5369334b0a..e04814caf3 100644 this._resettingPassword = false; -@@ -175,6 +194,14 @@ export class AuthServicesSSSDSwitchable extends AuthServices { +@@ -176,6 +199,14 @@ export class AuthServicesSSSDSwitchable extends AuthServices { }); } @@ -8516,7 +9422,7 @@ index 5369334b0a..e04814caf3 100644 _handleOnInfo(serviceName, info) { if (!this._eventExpected()) return; -@@ -243,6 +270,11 @@ export class AuthServicesSSSDSwitchable extends AuthServices { +@@ -255,6 +286,11 @@ export class AuthServicesSSSDSwitchable extends AuthServices { response = {password: answer}; break; } @@ -8528,8 +9434,8 @@ index 5369334b0a..e04814caf3 100644 case Constants.WEB_LOGIN_ROLE_NAME: { response = {}; break; -@@ -286,6 +318,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() { @@ -8537,7 +9443,11 @@ index 5369334b0a..e04814caf3 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; + } + @@ -8545,11 +9455,11 @@ index 5369334b0a..e04814caf3 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) { @@ -8574,13 +9484,13 @@ index 5369334b0a..e04814caf3 100644 const { serviceName, -- -2.51.0 +2.55.0 -From ea0ce9e4db98aa4a29dc1d6511499e858c73d388 Mon Sep 17 00:00:00 2001 +From aa69cb3547a570dfa88317ef60058e1b77cfea17 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Mon, 15 Sep 2025 16:36:42 +0200 -Subject: [PATCH 48/48] gdm: Add support for Passkey in +Subject: [PATCH 56/73] gdm: Add support for Passkey in authServicesSSSDSwitchable This allows selecting passkey authentication mechanism. @@ -8591,35 +9501,39 @@ about the passkey. It's used 'show-choice-list' with an emtpy list, to display the touch instruction. + +Part-of: --- - js/gdm/authServices.js | 12 ++++++++ - 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, 64 insertions(+), 1 deletion(-) + 5 files changed, 72 insertions(+), 1 deletion(-) diff --git a/js/gdm/authServices.js b/js/gdm/authServices.js -index a62a3de901..0a94f493a2 100644 +index b5a8d870f..6dd9bf221 100644 --- a/js/gdm/authServices.js +++ b/js/gdm/authServices.js -@@ -3,6 +3,7 @@ - import * as FingerprintManager from '../misc/fingerprintManager.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'; -+import * as PasskeyDeviceManager from '../misc/passkeyDeviceManager.js'; - import * as SmartcardManager from '../misc/smartcardManager.js'; ++import * as PasskeyDeviceManager from './passkeyDeviceManager.js'; + import * as SmartcardManager from './smartcardManager.js'; import {logErrorUnlessCancelled} from '../misc/errorUtils.js'; import * as Util from './util.js'; -@@ -89,6 +90,7 @@ export class AuthServices extends GObject.Object { - this._cancellable = null; - - this._connectSmartcardManager(); -+ this._connectPasskeyDeviceManager(); - this._connectFingerprintManager(); - } - -@@ -230,6 +232,14 @@ 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(); ++ if (this.supportedRoles.includes(Constants.PASSKEY_ROLE_NAME) && ++ this._enabledRoles.includes(Constants.PASSKEY_ROLE_NAME)) ++ this._connectPasskeyDeviceManager(); + if (this.supportedRoles.includes(Constants.FINGERPRINT_ROLE_NAME) && + this._enabledRoles.includes(Constants.FINGERPRINT_ROLE_NAME)) + this._connectFingerprintManager(); +@@ -264,6 +268,14 @@ export class AuthServices extends GObject.Object { this); } @@ -8634,7 +9548,7 @@ index a62a3de901..0a94f493a2 100644 _connectFingerprintManager() { // Fingerprint can only work on lockscreen if (!this._reauthOnly) -@@ -462,6 +472,8 @@ export class AuthServices extends GObject.Object { +@@ -500,6 +512,8 @@ export class AuthServices extends GObject.Object { _handleSmartcardChanged() {} @@ -8644,7 +9558,7 @@ index a62a3de901..0a94f493a2 100644 _handleOnInfo() {} diff --git a/js/gdm/authServicesSSSDSwitchable.js b/js/gdm/authServicesSSSDSwitchable.js -index e04814caf3..de03dec1d7 100644 +index 668637db0..c37ca3890 100644 --- a/js/gdm/authServicesSSSDSwitchable.js +++ b/js/gdm/authServicesSSSDSwitchable.js @@ -16,12 +16,14 @@ export class AuthServicesSSSDSwitchable extends AuthServices { @@ -8662,7 +9576,7 @@ index e04814caf3..de03dec1d7 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; @@ -8670,13 +9584,15 @@ index e04814caf3..de03dec1d7 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; @@ -8686,7 +9602,7 @@ index e04814caf3..de03dec1d7 100644 case Constants.WEB_LOGIN_ROLE_NAME: this._startWebLogin(); break; -@@ -202,6 +214,14 @@ export class AuthServicesSSSDSwitchable extends AuthServices { +@@ -207,6 +221,14 @@ export class AuthServicesSSSDSwitchable extends AuthServices { this.emit('reset', {softReset: true, reuseEntryText: true}); } @@ -8701,7 +9617,7 @@ index e04814caf3..de03dec1d7 100644 _handleOnInfo(serviceName, info) { if (!this._eventExpected()) return; -@@ -262,7 +282,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { +@@ -278,7 +300,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { } _formatResponse(answer) { @@ -8710,7 +9626,7 @@ index e04814caf3..de03dec1d7 100644 let response; switch (role) { -@@ -275,6 +295,10 @@ export class AuthServicesSSSDSwitchable extends AuthServices { +@@ -291,6 +313,10 @@ export class AuthServicesSSSDSwitchable extends AuthServices { response = {pin: answer, tokenName, moduleName, keyId, label}; break; } @@ -8721,7 +9637,7 @@ index e04814caf3..de03dec1d7 100644 case Constants.WEB_LOGIN_ROLE_NAME: { response = {}; break; -@@ -356,6 +380,26 @@ export class AuthServicesSSSDSwitchable extends AuthServices { +@@ -376,6 +402,30 @@ export class AuthServicesSSSDSwitchable extends AuthServices { }; } @@ -8733,15 +9649,19 @@ index e04814caf3..de03dec1d7 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, ++ }); + } + } + @@ -8749,10 +9669,10 @@ index e04814caf3..de03dec1d7 100644 const { serviceName, diff --git a/js/gdm/constants.js b/js/gdm/constants.js -index e8ca48625a..7c48599e64 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'; @@ -8761,10 +9681,10 @@ index e8ca48625a..7c48599e64 100644 export const PASSWORD_SERVICE_NAME = 'gdm-password'; diff --git a/js/gdm/util.js b/js/gdm/util.js -index 6c160d323b..743fce11b7 100644 +index 36ff70427..998ce8ff1 100644 --- a/js/gdm/util.js +++ b/js/gdm/util.js -@@ -19,6 +19,7 @@ var LOGIN_SCREEN_SCHEMA = 'org.gnome.login-screen'; +@@ -20,6 +20,7 @@ var LOGIN_SCREEN_SCHEMA = 'org.gnome.login-screen'; var PASSWORD_AUTHENTICATION_KEY = 'enable-password-authentication'; var FINGERPRINT_AUTHENTICATION_KEY = 'enable-fingerprint-authentication'; var SMARTCARD_AUTHENTICATION_KEY = 'enable-smartcard-authentication'; @@ -8772,7 +9692,7 @@ index 6c160d323b..743fce11b7 100644 var SWITCHABLE_AUTHENTICATION_KEY = 'enable-switchable-authentication'; var WEB_AUTHENTICATION_KEY = 'enable-web-authentication'; var BANNER_MESSAGE_KEY = 'banner-message-enable'; -@@ -139,6 +140,7 @@ export function isSelectable(mechanism) { +@@ -148,6 +149,7 @@ function isSelectable(mechanism) { switch (mechanism.role) { case Constants.PASSWORD_ROLE_NAME: case Constants.SMARTCARD_ROLE_NAME: @@ -8780,7 +9700,7 @@ index 6c160d323b..743fce11b7 100644 case Constants.WEB_LOGIN_ROLE_NAME: return true; case Constants.FINGERPRINT_ROLE_NAME: -@@ -449,6 +451,8 @@ var ShellUserVerifier = class { +@@ -482,6 +484,8 @@ var ShellUserVerifier = class { enabledRoles.push(Constants.PASSWORD_ROLE_NAME); if (this._settings.get_boolean(SMARTCARD_AUTHENTICATION_KEY)) enabledRoles.push(Constants.SMARTCARD_ROLE_NAME); @@ -8790,18 +9710,5171 @@ index 6c160d323b..743fce11b7 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 bebc7ad40a..654959190e 100644 +index da24bed05..0fa0ce801 100644 --- a/js/ui/unlockDialog.js +++ b/js/ui/unlockDialog.js -@@ -398,6 +398,8 @@ class UnlockDialogClock extends St.BoxLayout { +@@ -399,6 +399,8 @@ class UnlockDialogClock extends St.BoxLayout { - if (authMechanism?.role === GdmConstants.SMARTCARD_ROLE_NAME) + if (selectedAuthRole === GdmConstants.SMARTCARD_ROLE_NAME) text = _('Insert smartcard'); -+ else if (authMechanism?.role === GdmConstants.PASSKEY_ROLE_NAME) ++ else if (selectedAuthRole === GdmConstants.PASSKEY_ROLE_NAME) + text = _('Insert security key'); else if (this._seat.touch_mode) text = _('Swipe up'); else -- -2.51.0 +2.55.0 + + +From 441ae5e441a902063199fe1797c2f71f73950d9e Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Wed, 8 Jul 2026 20:01:04 +0200 +Subject: [PATCH 57/73] 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 998ce8ff1..4f91bd200 100644 +--- a/js/gdm/util.js ++++ b/js/gdm/util.js +@@ -21,7 +21,6 @@ var PASSWORD_AUTHENTICATION_KEY = 'enable-password-authentication'; + var FINGERPRINT_AUTHENTICATION_KEY = 'enable-fingerprint-authentication'; + var SMARTCARD_AUTHENTICATION_KEY = 'enable-smartcard-authentication'; + var PASSKEY_AUTHENTICATION_KEY = 'enable-passkey-authentication'; +-var SWITCHABLE_AUTHENTICATION_KEY = 'enable-switchable-authentication'; + var WEB_AUTHENTICATION_KEY = 'enable-web-authentication'; + var BANNER_MESSAGE_KEY = 'banner-message-enable'; + var BANNER_MESSAGE_TEXT_KEY = 'banner-message-text'; +@@ -96,6 +95,14 @@ function fadeOutActor(actor) { + return hold; + } + ++// 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. + * +@@ -184,6 +191,7 @@ var ShellUserVerifier = class { + + this._client = client; + this._cancellable = null; ++ this._authServices = []; + + this._messageQueue = []; + this._messageQueueTimeoutId = 0; +@@ -212,8 +220,10 @@ var ShellUserVerifier = class { + 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); +@@ -225,20 +235,18 @@ var ShellUserVerifier = class { + } + + 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); + +@@ -246,8 +254,7 @@ var ShellUserVerifier = class { + } + + cancel() { +- this._authServicesSSSDSwitchable?.cancel(); +- this._authServicesLegacy?.cancel(); ++ this._authServices.forEach(s => s.cancel()); + + this._userVerifier?.call_cancel_sync(null); + +@@ -255,13 +262,8 @@ var ShellUserVerifier = class { + } + + 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(); + +@@ -285,11 +287,8 @@ var ShellUserVerifier = class { + } + + destroy() { +- this._authServicesSSSDSwitchable?.destroy(); +- this._authServicesSSSDSwitchable = null; +- +- this._authServicesLegacy?.destroy(); +- this._authServicesLegacy = null; ++ this._authServices.forEach(s => s.destroy()); ++ this._authServices = []; + + this.cancel(); + +@@ -298,8 +297,7 @@ var ShellUserVerifier = class { + } + + 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) { +@@ -307,16 +305,15 @@ var ShellUserVerifier = class { + // 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) { +@@ -491,15 +488,16 @@ var ShellUserVerifier = class { + 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(); + } +@@ -509,36 +507,34 @@ var ShellUserVerifier = class { + + 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), +@@ -561,18 +557,40 @@ var ShellUserVerifier = class { + } + + 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)) ?? +@@ -581,14 +599,6 @@ var ShellUserVerifier = class { + 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 f991a98abb12448f6a72d4c6fc462d5c25295552 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Sun, 19 Jul 2026 13:31:48 +0200 +Subject: [PATCH 58/73] 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 4f91bd200..8ea61e42c 100644 +--- a/js/gdm/util.js ++++ b/js/gdm/util.js +@@ -17,11 +17,6 @@ var FADE_ANIMATION_TIME = 160; + var CLONE_FADE_ANIMATION_TIME = 250; + + var LOGIN_SCREEN_SCHEMA = 'org.gnome.login-screen'; +-var PASSWORD_AUTHENTICATION_KEY = 'enable-password-authentication'; +-var FINGERPRINT_AUTHENTICATION_KEY = 'enable-fingerprint-authentication'; +-var SMARTCARD_AUTHENTICATION_KEY = 'enable-smartcard-authentication'; +-var PASSKEY_AUTHENTICATION_KEY = 'enable-passkey-authentication'; +-var WEB_AUTHENTICATION_KEY = 'enable-web-authentication'; + var BANNER_MESSAGE_KEY = 'banner-message-enable'; + var BANNER_MESSAGE_TEXT_KEY = 'banner-message-text'; + var ALLOWED_FAILURES_KEY = 'allowed-failures'; +@@ -475,30 +470,14 @@ var ShellUserVerifier = class { + } + + _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(); + } + +@@ -509,21 +488,14 @@ var ShellUserVerifier = class { + 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(); + } + +@@ -567,13 +539,13 @@ var ShellUserVerifier = class { + + 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 dc086a40b0890741be3867943df01dc6712626fa Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Sun, 19 Jul 2026 15:25:00 +0200 +Subject: [PATCH 59/73] 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 | 41 ++---------------------------------- + js/ui/unlockDialog.js | 34 ++++++------------------------ + 6 files changed, 40 insertions(+), 70 deletions(-) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index a00feed40..2962e6ada 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -6,7 +6,6 @@ const { Clutter, Gio, GLib, GObject, Meta, Pango, Shell, St } = imports.gi; + const Animation = imports.ui.animation; + const AuthList = imports.gdm.authList; + const Batch = imports.gdm.batch; +-const Constants = imports.gdm.constants; + const GdmUtil = imports.gdm.util; + const OVirt = imports.gdm.oVirt; + const Vmware = imports.gdm.vmware; +@@ -559,7 +558,7 @@ var 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 f5e439838..5bcf43d92 100644 +--- a/js/gdm/loginDialog.js ++++ b/js/gdm/loginDialog.js +@@ -918,7 +918,7 @@ var 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 8ea61e42c..4fd91e1fa 100644 +--- a/js/gdm/util.js ++++ b/js/gdm/util.js +@@ -1,7 +1,6 @@ + // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + /* exported BANNER_MESSAGE_KEY, BANNER_MESSAGE_TEXT_KEY, LOGO_KEY, +- DISABLE_USER_LIST_KEY, fadeInActor, fadeOutActor, cloneAndFadeOutActor, +- isSelectable, getNonSelectableIconName */ ++ DISABLE_USER_LIST_KEY, fadeInActor, fadeOutActor, cloneAndFadeOutActor */ + + const { Clutter, Gio, GLib } = imports.gi; + const Signals = imports.signals; +@@ -143,42 +142,6 @@ function cloneAndFadeOutActor(actor) { + return hold; + } + +-/** +- * @param {object} mechanism +- * @returns {boolean} +- */ +-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} +- */ +-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}`); +- } +-} +- + var ShellUserVerifier = class { + constructor(client, params) { + params = Params.parse(params, { reauthenticationOnly: false }); +@@ -565,7 +528,7 @@ var ShellUserVerifier = class { + .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 0fa0ce801..3df9b0e64 100644 +--- a/js/ui/unlockDialog.js ++++ b/js/ui/unlockDialog.js +@@ -12,8 +12,6 @@ const SwipeTracker = imports.ui.swipeTracker; + + const AuthMenuButton = imports.gdm.authMenuButton; + const AuthPrompt = imports.gdm.authPrompt; +-const GdmConstants = imports.gdm.constants; +-const GdmUtil = imports.gdm.util; + + const PRIMARY_UNLOCK_METHOD_SECTION_NAME = _('Unlock Options'); + +@@ -388,19 +386,16 @@ class UnlockDialogClock extends St.BoxLayout { + this._date.text = date.toLocaleFormat(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 +@@ -724,7 +719,7 @@ var UnlockDialog = GObject.registerClass({ + + this._selectedAuthMechanism = authMechanism; + +- this._clock.selectAuthRole(authMechanism?.role); ++ this._clock.selectAuthHint(authMechanism?.hint); + } + + _createBackground(monitorIndex) { +@@ -910,17 +905,13 @@ var 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); + } + } + +@@ -930,17 +921,6 @@ var 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 491cec5afc1c1e1ec7ec4b4bb197677c08ae5cee Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Sun, 19 Jul 2026 15:35:40 +0200 +Subject: [PATCH 60/73] 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 beee88568..024d15abf 100644 +--- a/js/js-resources.gresource.xml ++++ b/js/js-resources.gresource.xml +@@ -14,7 +14,7 @@ + gdm/oVirt.js + gdm/credentialManager.js + gdm/vmware.js +- gdm/passkeyDeviceManager.js ++ gdm/fido2TokenManager.js + gdm/realmd.js + gdm/smartcardManager.js + gdm/util.js +-- +2.55.0 + + +From bc7659d8b5773676f545f737be465cda51338b30 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Mon, 20 Jul 2026 19:28:48 +0200 +Subject: [PATCH 61/73] 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 c7ad66d4f683b310a324e8337f04a264da3a40c8 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Mon, 20 Jul 2026 20:02:22 +0200 +Subject: [PATCH 62/73] 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, 98 insertions(+), 94 deletions(-) + delete mode 100644 js/gdm/constants.js + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index 2962e6ada..c204bff5f 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -458,11 +458,10 @@ var AuthPrompt = GObject.registerClass({ + this.emit('prompted'); + } + +- _onShowMessage({serviceName, message, type}) { ++ _onShowMessage({message, type, shouldWiggle}) { + let wiggleParameters = {duration: 0}; + +- if (type === GdmUtil.MessageType.ERROR && +- this._userVerifier.serviceIsFingerprint(serviceName)) { ++ if (shouldWiggle) { + // TODO: Use Await for wiggle to be over before unfreezing the user verifier queue + wiggleParameters = { + duration: 65, +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 4fd91e1fa..84bc2ccea 100644 +--- a/js/gdm/util.js ++++ b/js/gdm/util.js +@@ -6,7 +6,6 @@ const { Clutter, Gio, GLib } = imports.gi; + const Signals = imports.signals; + + const Batch = imports.gdm.batch; +-const Constants = imports.gdm.constants; + const Main = imports.ui.main; + const Params = imports.misc.params; + const {AuthServicesLegacy} = imports.gdm.authServicesLegacy; +@@ -316,9 +315,9 @@ var ShellUserVerifier = class { + + delete this._currentMessageExtraInterval; + this.emit('show-message', { +- serviceName: message.serviceName, + message: message.text, + type: message.type, ++ shouldWiggle: message.wiggle, + }); + + this._messageQueueTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, +@@ -337,14 +336,20 @@ var ShellUserVerifier = class { + 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; +@@ -355,7 +360,7 @@ var ShellUserVerifier = class { + this._clearMessageQueue(); + + this._messageQueue = newQueue; +- this._queueMessage({serviceName, message, messageType}); ++ this._queueMessage({serviceName, message, messageType, wiggle}); + } + + _clearMessageQueue() { +@@ -424,10 +429,6 @@ var ShellUserVerifier = class { + 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 024d15abf..975f64080 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/fingerprintManager.js + gdm/loginDialog.js + gdm/oVirt.js +-- +2.55.0 + + +From 3871f9e0aa5296cb9d9bd0eb1f3f7392c301bb05 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/73] 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 c204bff5f..bdb73a54b 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -53,6 +53,16 @@ var 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({ +@@ -68,7 +78,8 @@ var AuthPrompt = GObject.registerClass({ + this._mode = mode; + this._defaultButtonWellActor = null; + this._cancelledRetries = 0; +- this._promptStep = 0; ++ ++ this.connect('notify::prompt-step', () => this._updateCancelButton()); + + this._idleMonitor = Meta.IdleMonitor.get_core(); + +@@ -327,7 +338,7 @@ var 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 @@ var AuthPrompt = GObject.registerClass({ + this.clear(); + + this._queryingService = serviceName; +- this._promptStep++; +- this._updateCancelButton(); ++ this.promptStep++; + + const preemptiveAnswer = this._preemptiveAnswer; + this._clearPreemptiveState(); +@@ -448,8 +458,7 @@ var AuthPrompt = GObject.registerClass({ + this.clear(); + + this._queryingService = serviceName; +- this._promptStep++; +- this._updateCancelButton(); ++ this.promptStep++; + + this._clearPreemptiveState(); + +@@ -492,7 +501,7 @@ var AuthPrompt = GObject.registerClass({ + this.clear(); + + this._queryingService = serviceName; +- this._promptStep++; ++ this.promptStep++; + + this._clearPreemptiveState(); + +@@ -513,8 +522,7 @@ var AuthPrompt = GObject.registerClass({ + this.clear(); + + this._queryingService = serviceName; +- this._promptStep++; +- this._updateCancelButton(); ++ this.promptStep++; + + this._webLoginParams = {message, url, code, buttons}; + +@@ -840,12 +848,10 @@ var 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; + } +@@ -860,8 +866,7 @@ var 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); +@@ -953,7 +958,7 @@ var 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 5bcf43d92..59dc657bd 100644 +--- a/js/gdm/loginDialog.js ++++ b/js/gdm/loginDialog.js +@@ -365,6 +365,8 @@ var 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); + +@@ -982,9 +984,7 @@ var 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 727bb734ad390f9799728aa7e399bc897b9adcf0 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/73] 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 bdb73a54b..25e5ebfbf 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -153,7 +153,7 @@ var AuthPrompt = GObject.registerClass({ + + vfunc_key_press_event(keyPressEvent) { + if (keyPressEvent.keyval == Clutter.KEY_Escape) +- this.cancel(); ++ this._handleCancel(); + return super.vfunc_key_press_event(keyPressEvent); + } + +@@ -174,7 +174,7 @@ var AuthPrompt = GObject.registerClass({ + y_align: Clutter.ActorAlign.CENTER, + child: new St.Icon({ 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); + +@@ -953,6 +953,13 @@ var 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 84bc2ccea..4c9d8c7c1 100644 +--- a/js/gdm/util.js ++++ b/js/gdm/util.js +@@ -210,6 +210,12 @@ var ShellUserVerifier = class { + 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 4a111078fd4a574c383aee916d90b1115d35598f 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/73] 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 25e5ebfbf..8dadfff47 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -306,14 +306,7 @@ var 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); + +@@ -954,8 +947,15 @@ var 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 d6110521d63a796124cfa05dba47255bb695e6fa 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/73] 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 5f4d9465965916932c94122fbaacbade8dc44838 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/73] 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 422927864582d7292aa90b39a46c66251527a5a4 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/73] 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 4c9d8c7c1..0811cacf5 100644 +--- a/js/gdm/util.js ++++ b/js/gdm/util.js +@@ -531,8 +531,14 @@ var ShellUserVerifier = class { + // 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 41189415e70643482c21c7e5e1efd807ceeb3852 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/73] 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 8dadfff47..246b42ea6 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -6,11 +6,11 @@ const { Clutter, Gio, GLib, GObject, Meta, Pango, Shell, St } = imports.gi; + const Animation = imports.ui.animation; + const AuthList = imports.gdm.authList; + const Batch = imports.gdm.batch; +-const GdmUtil = imports.gdm.util; + const OVirt = imports.gdm.oVirt; + const Vmware = imports.gdm.vmware; + const Params = imports.misc.params; + const ShellEntry = imports.ui.shellEntry; ++const UserVerifier = imports.gdm.userVerifier; + const UserWidget = imports.ui.userWidget; + const WebLogin = imports.gdm.webLogin; + const Util = imports.misc.util; +@@ -89,7 +89,7 @@ var AuthPrompt = GObject.registerClass({ + else if (this._mode == AuthPromptMode.UNLOCK_OR_LOG_IN) + reauthenticationOnly = false; + +- this._userVerifier = new GdmUtil.ShellUserVerifier(this._gdmClient, { reauthenticationOnly }); ++ this._userVerifier = new UserVerifier.ShellUserVerifier(this._gdmClient, { reauthenticationOnly }); + + this._userVerifier.connectObject( + 'ask-question', (_, args) => this._onAskQuestion(args), +@@ -479,7 +479,7 @@ var 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 && +@@ -754,12 +754,12 @@ var AuthPrompt = GObject.registerClass({ + } + + setMessage(message, type, wiggleParameters = {duration: 0}) { +- 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 59dc657bd..42c697c9a 100644 +--- a/js/gdm/loginDialog.js ++++ b/js/gdm/loginDialog.js +@@ -25,7 +25,7 @@ const AuthPrompt = imports.gdm.authPrompt; + const Batch = imports.gdm.batch; + const BoxPointer = imports.ui.boxpointer; + const CtrlAltTab = imports.ui.ctrlAltTab; +-const GdmUtil = imports.gdm.util; ++const GdmUtil = imports.gdm.userVerifier; + const Layout = imports.ui.layout; + const LoginManager = imports.misc.loginManager; + const Main = imports.ui.main; +diff --git a/js/gdm/userVerifier.js b/js/gdm/userVerifier.js +new file mode 100644 +index 0000000..0811cac +--- /dev/null ++++ b/js/gdm/userVerifier.js +@@ -0,0 +1,575 @@ ++// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- ++/* exported BANNER_MESSAGE_KEY, BANNER_MESSAGE_TEXT_KEY, LOGO_KEY, ++ DISABLE_USER_LIST_KEY, fadeInActor, fadeOutActor, cloneAndFadeOutActor */ ++ ++const { Clutter, Gio, GLib } = imports.gi; ++const Signals = imports.signals; ++ ++const Batch = imports.gdm.batch; ++const Main = imports.ui.main; ++const Params = imports.misc.params; ++const {AuthServicesLegacy} = imports.gdm.authServicesLegacy; ++const {AuthServicesSSSDSwitchable} = imports.gdm.authServicesSSSDSwitchable; ++ ++var FADE_ANIMATION_TIME = 160; ++var CLONE_FADE_ANIMATION_TIME = 250; ++ ++var LOGIN_SCREEN_SCHEMA = 'org.gnome.login-screen'; ++var BANNER_MESSAGE_KEY = 'banner-message-enable'; ++var BANNER_MESSAGE_TEXT_KEY = 'banner-message-text'; ++var ALLOWED_FAILURES_KEY = 'allowed-failures'; ++ ++var LOGO_KEY = 'logo'; ++var DISABLE_USER_LIST_KEY = 'disable-user-list'; ++ ++// Give user 48ms to read each character of a PAM message ++var USER_READ_TIME = 48; ++var 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, ++}; ++ ++function fadeInActor(actor) { ++ if (actor.opacity == 255 && actor.visible) ++ return null; ++ ++ let hold = new Batch.Hold(); ++ actor.show(); ++ let [, naturalHeight] = actor.get_preferred_height(-1); ++ ++ actor.opacity = 0; ++ actor.set_height(0); ++ actor.ease({ ++ opacity: 255, ++ height: naturalHeight, ++ duration: FADE_ANIMATION_TIME, ++ mode: Clutter.AnimationMode.EASE_OUT_QUAD, ++ onComplete: () => { ++ this.set_height(-1); ++ hold.release(); ++ }, ++ }); ++ ++ return hold; ++} ++ ++function fadeOutActor(actor) { ++ if (!actor.visible || actor.opacity == 0) { ++ actor.opacity = 0; ++ actor.hide(); ++ return null; ++ } ++ ++ let hold = new Batch.Hold(); ++ actor.ease({ ++ opacity: 0, ++ height: 0, ++ duration: FADE_ANIMATION_TIME, ++ mode: Clutter.AnimationMode.EASE_OUT_QUAD, ++ onComplete: () => { ++ this.hide(); ++ this.set_height(-1); ++ hold.release(); ++ }, ++ }); ++ return hold; ++} ++ ++// 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 ++ */ ++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(); ++ ++ let 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; ++} ++ ++var ShellUserVerifier = class { ++ constructor(client, params) { ++ 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 message.length * USER_READ_TIME * MESSAGE_TIME_MULTIPLIER; ++ } ++ ++ _finishMessageQueue() { ++ if (!this.hasPendingMessages) ++ return; ++ ++ this._messageQueue = []; ++ ++ 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); ++ } ++ ++ _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}); ++ } ++ ++ _queueMessageTimeout() { ++ if (this._messageQueueTimeoutId != 0) ++ return; ++ ++ const message = this.currentMessage; ++ ++ delete this._currentMessageExtraInterval; ++ this.emit('show-message', { ++ message: message.text, ++ type: message.type, ++ shouldWiggle: message.wiggle, ++ }); ++ ++ this._messageQueueTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, ++ message.interval + (this._currentMessageExtraInterval | 0), () => { ++ 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; ++ } ++ ++ 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) => { ++ const 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(); ++ }); ++ }); ++ } ++}; ++Signals.addSignalMethods(ShellUserVerifier.prototype); +diff --git a/js/gdm/util.js b/js/gdm/util.js +deleted file mode 100644 +index 0811cac..0000000 +--- a/js/gdm/util.js ++++ /dev/null +@@ -1,575 +0,0 @@ +-// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- +-/* exported BANNER_MESSAGE_KEY, BANNER_MESSAGE_TEXT_KEY, LOGO_KEY, +- DISABLE_USER_LIST_KEY, fadeInActor, fadeOutActor, cloneAndFadeOutActor */ +- +-const { Clutter, Gio, GLib } = imports.gi; +-const Signals = imports.signals; +- +-const Batch = imports.gdm.batch; +-const Main = imports.ui.main; +-const Params = imports.misc.params; +-const {AuthServicesLegacy} = imports.gdm.authServicesLegacy; +-const {AuthServicesSSSDSwitchable} = imports.gdm.authServicesSSSDSwitchable; +- +-var FADE_ANIMATION_TIME = 160; +-var CLONE_FADE_ANIMATION_TIME = 250; +- +-var LOGIN_SCREEN_SCHEMA = 'org.gnome.login-screen'; +-var BANNER_MESSAGE_KEY = 'banner-message-enable'; +-var BANNER_MESSAGE_TEXT_KEY = 'banner-message-text'; +-var ALLOWED_FAILURES_KEY = 'allowed-failures'; +- +-var LOGO_KEY = 'logo'; +-var DISABLE_USER_LIST_KEY = 'disable-user-list'; +- +-// Give user 48ms to read each character of a PAM message +-var USER_READ_TIME = 48; +-var 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, +-}; +- +-function fadeInActor(actor) { +- if (actor.opacity == 255 && actor.visible) +- return null; +- +- let hold = new Batch.Hold(); +- actor.show(); +- let [, naturalHeight] = actor.get_preferred_height(-1); +- +- actor.opacity = 0; +- actor.set_height(0); +- actor.ease({ +- opacity: 255, +- height: naturalHeight, +- duration: FADE_ANIMATION_TIME, +- mode: Clutter.AnimationMode.EASE_OUT_QUAD, +- onComplete: () => { +- this.set_height(-1); +- hold.release(); +- }, +- }); +- +- return hold; +-} +- +-function fadeOutActor(actor) { +- if (!actor.visible || actor.opacity == 0) { +- actor.opacity = 0; +- actor.hide(); +- return null; +- } +- +- let hold = new Batch.Hold(); +- actor.ease({ +- opacity: 0, +- height: 0, +- duration: FADE_ANIMATION_TIME, +- mode: Clutter.AnimationMode.EASE_OUT_QUAD, +- onComplete: () => { +- this.hide(); +- this.set_height(-1); +- hold.release(); +- }, +- }); +- return hold; +-} +- +-// 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 +- */ +-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(); +- +- let 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; +-} +- +-var ShellUserVerifier = class { +- constructor(client, params) { +- 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 message.length * USER_READ_TIME * MESSAGE_TIME_MULTIPLIER; +- } +- +- _finishMessageQueue() { +- if (!this.hasPendingMessages) +- return; +- +- this._messageQueue = []; +- +- 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); +- } +- +- _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}); +- } +- +- _queueMessageTimeout() { +- if (this._messageQueueTimeoutId != 0) +- return; +- +- const message = this.currentMessage; +- +- delete this._currentMessageExtraInterval; +- this.emit('show-message', { +- message: message.text, +- type: message.type, +- shouldWiggle: message.wiggle, +- }); +- +- this._messageQueueTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, +- message.interval + (this._currentMessageExtraInterval | 0), () => { +- 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; +- } +- +- 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) => { +- const 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(); +- }); +- }); +- } +-}; +-Signals.addSignalMethods(ShellUserVerifier.prototype); +diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml +index 975f64080..78da13263 100644 +--- a/js/js-resources.gresource.xml ++++ b/js/js-resources.gresource.xml +@@ -16,7 +16,7 @@ + gdm/fido2TokenManager.js + gdm/realmd.js + gdm/smartcardManager.js +- gdm/util.js ++ gdm/userVerifier.js + gdm/webLogin.js + + misc/config.js +diff --git a/po/POTFILES.in b/po/POTFILES.in +index 5786f7436..ae7cc76af 100644 +--- a/po/POTFILES.in ++++ b/po/POTFILES.in +@@ -11,6 +11,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/systemActions.js + js/misc/util.js +-- +2.55.0 + + +From e390a4a165d90a4c5ee4b118824d4dd07f58a99d 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/73] 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 | 24 ++++++++------- + js/gdm/settings.js | 14 +++++++++ + js/gdm/userVerifier.js | 44 ++-------------------------- + js/gdm/util.js | 39 ++++++++++++++++++++++++ + js/js-resources.gresource.xml | 2 ++ + 7 files changed, 79 insertions(+), 64 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 42c697c9a..cb62847ad 100644 +--- a/js/gdm/loginDialog.js ++++ b/js/gdm/loginDialog.js +@@ -25,13 +25,15 @@ const AuthPrompt = imports.gdm.authPrompt; + const Batch = imports.gdm.batch; + const BoxPointer = imports.ui.boxpointer; + const CtrlAltTab = imports.ui.ctrlAltTab; +-const GdmUtil = imports.gdm.userVerifier; ++const GdmUtil = imports.gdm.util; ++const Settings = imports.gdm.settings; + const Layout = imports.ui.layout; + const LoginManager = imports.misc.loginManager; + const Main = imports.ui.main; + const PopupMenu = imports.ui.popupMenu; + const Realmd = imports.gdm.realmd; + const UserWidget = imports.ui.userWidget; ++const {MessageType} = imports.gdm.userVerifier; + + const _FADE_ANIMATION_TIME = 250; + const _SCROLL_ANIMATION_TIME = 500; +@@ -334,15 +336,15 @@ var LoginDialog = GObject.registerClass({ + } catch (e) { + } + +- 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::%s'.format(GdmUtil.BANNER_MESSAGE_KEY), ++ this._settings.connect('changed::%s'.format(Settings.BANNER_MESSAGE_KEY), + this._updateBanner.bind(this)); +- this._settings.connect('changed::%s'.format(GdmUtil.BANNER_MESSAGE_TEXT_KEY), ++ this._settings.connect('changed::%s'.format(Settings.BANNER_MESSAGE_TEXT_KEY), + this._updateBanner.bind(this)); +- this._settings.connect('changed::%s'.format(GdmUtil.DISABLE_USER_LIST_KEY), ++ this._settings.connect('changed::%s'.format(Settings.DISABLE_USER_LIST_KEY), + this._updateDisableUserList.bind(this)); +- this._settings.connect('changed::%s'.format(GdmUtil.LOGO_KEY), ++ this._settings.connect('changed::%s'.format(Settings.LOGO_KEY), + this._updateLogo.bind(this)); + + this._textureCache = St.TextureCache.get_default(); +@@ -758,7 +760,7 @@ var 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) +@@ -789,8 +791,8 @@ var LoginDialog = GObject.registerClass({ + } + + _updateBanner() { +- let enabled = this._settings.get_boolean(GdmUtil.BANNER_MESSAGE_KEY); +- let text = this._settings.get_string(GdmUtil.BANNER_MESSAGE_TEXT_KEY); ++ let enabled = this._settings.get_boolean(Settings.BANNER_MESSAGE_KEY); ++ let text = this._settings.get_string(Settings.BANNER_MESSAGE_TEXT_KEY); + + if (enabled && text) { + this._bannerLabel.set_text(text); +@@ -831,7 +833,7 @@ var 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); +@@ -962,7 +964,7 @@ var 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 0811cacf5..8baad81c9 100644 +--- a/js/gdm/userVerifier.js ++++ b/js/gdm/userVerifier.js +@@ -1,6 +1,6 @@ + // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + /* exported BANNER_MESSAGE_KEY, BANNER_MESSAGE_TEXT_KEY, LOGO_KEY, +- DISABLE_USER_LIST_KEY, fadeInActor, fadeOutActor, cloneAndFadeOutActor */ ++ DISABLE_USER_LIST_KEY, fadeInActor, fadeOutActor */ + + const { Clutter, Gio, GLib } = imports.gi; + const Signals = imports.signals; +@@ -12,7 +12,6 @@ const {AuthServicesLegacy} = imports.gdm.authServicesLegacy; + const {AuthServicesSSSDSwitchable} = imports.gdm.authServicesSSSDSwitchable; + + var FADE_ANIMATION_TIME = 160; +-var CLONE_FADE_ANIMATION_TIME = 250; + + var LOGIN_SCREEN_SCHEMA = 'org.gnome.login-screen'; + var BANNER_MESSAGE_KEY = 'banner-message-enable'; +@@ -110,37 +109,6 @@ export class InitError extends Error { + } + } + +-/** +- * @param {Clutter.Actor} actor +- */ +-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(); +- +- let 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; +-} +- + var ShellUserVerifier = class { + constructor(client, params) { + params = Params.parse(params, { reauthenticationOnly: false }); +@@ -531,14 +499,8 @@ var ShellUserVerifier = class { + // 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; +- }); ++ .flatMap(s => s.enabledMechanisms ?? []) ++ .filter(m => !seenRoles.has(m.role) && seenRoles.add(m.role)); + + const selectedMechanism = this.selectedMechanism ?? + mechanisms.find(m => m.selectable) ?? +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 78da13263..cf70dd2fd 100644 +--- a/js/js-resources.gresource.xml ++++ b/js/js-resources.gresource.xml +@@ -15,8 +15,10 @@ + gdm/vmware.js + gdm/fido2TokenManager.js + gdm/realmd.js ++ gdm/settings.js + gdm/smartcardManager.js + gdm/userVerifier.js ++ gdm/util.js + gdm/webLogin.js + + misc/config.js +-- +2.55.0 + + +From aec1913e3c50bd315b579782823de2278432a8e3 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/73] 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 246b42ea6..ffc73c9dc 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -180,20 +180,7 @@ var 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({ +@@ -303,6 +290,7 @@ var 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 @@ var 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 @@ var 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 @@ var AuthPrompt = GObject.registerClass({ + this.emit('prompted'); + } + +- _onShowChoiceList({serviceName, promptMessage, choiceList}) { ++ _onShowChoiceList({serviceName, promptMessage, choiceList, choiceHandler}) { + if (this._queryingService) + this.clear(); + +@@ -455,6 +463,9 @@ var AuthPrompt = GObject.registerClass({ + + this._clearPreemptiveState(); + ++ this._connectAuthListActivate(); ++ this._setPendingCallback(choiceHandler); ++ + this.setChoiceList(promptMessage, choiceList); + this.updateSensitivity({sensitive: true}); + this.emit('prompted'); +@@ -498,10 +509,7 @@ var AuthPrompt = GObject.registerClass({ + + this._clearPreemptiveState(); + +- const authButtonClickedId = this._authButton.connect('clicked', () => { +- this._authButton.disconnect(authButtonClickedId); +- callback(); +- }); ++ this._setPendingCallback(callback); + + this._authButton.set_label(label); + +@@ -676,8 +684,10 @@ var 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; +@@ -701,6 +711,34 @@ var 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 8baad81c9..0f0ab021a 100644 +--- a/js/gdm/userVerifier.js ++++ b/js/gdm/userVerifier.js +@@ -227,18 +227,6 @@ var ShellUserVerifier = class { + 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)); + } +@@ -511,14 +499,14 @@ var ShellUserVerifier = class { + + 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 b0c89ed0ef2678de2e02b51ea66054f87c08a80e Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Tue, 28 Jul 2026 16:39:32 +0200 +Subject: [PATCH 72/73] 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 0f0ab021a..5031b3352 100644 +--- a/js/gdm/userVerifier.js ++++ b/js/gdm/userVerifier.js +@@ -117,6 +117,7 @@ var ShellUserVerifier = class { + this._client = client; + this._cancellable = null; + this._authServices = []; ++ this._driverService = null; + + this._messageQueue = []; + this._messageQueueTimeoutId = 0; +@@ -170,6 +171,15 @@ var ShellUserVerifier = class { + 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()); + +@@ -396,9 +406,14 @@ var ShellUserVerifier = class { + } + + _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 + +From cbd27907986975d938c7e94e3146b97fd193586e Mon Sep 17 00:00:00 2001 +From: Adrian Vovk +Date: Thu, 13 Aug 2026 14:45:53 -0400 +Subject: [PATCH 73/73] gdm/userVerifier: Fix mechanisms-changed race + +Starting with GDM e760c8e4, GDM will return an error from +BeginVerification() whenever `gdm-switchable-auth` is unavailable. + +This would cause AuthServicesSSSDSwitchable to emit `mechanisms-changed` +during ShellUserVerifier.begin(). The signal would cause a chain of +events that leads to ShellUserVerifier.cancel() being called inside of +that same call stack. In response to this, GDM will send us a reset +signal. Meanwhile, the call stack would move on and try to call +BeginVerification() for AuthServicesLegacy. + +In the unhappy case, we'd receive the reset signal before +BeginVerification() is done, which completely de-syncs gnome-shell's and +GDM's views of what's going on. Ultimately the password box would never +appear. gnome-shell would kick the user right back into the user list, +but GDM would think that gdm-password is currently ongoing. + +The fix is to delay the propagation of `mechanisms-changed` until we're +done with BeginVerification() for all available auth services. + +Closes: https://gitlab.gnome.org/GNOME/gdm/-/work_items/1089 +Part-of: +--- + js/gdm/userVerifier.js | 15 +++++++++++++-- + 1 file changed, 13 insertions(+), 2 deletions(-) + +diff --git a/js/gdm/userVerifier.js b/js/gdm/userVerifier.js +index 5031b3352..c1e186480 100644 +--- a/js/gdm/userVerifier.js ++++ b/js/gdm/userVerifier.js +@@ -119,6 +119,9 @@ var ShellUserVerifier = class { + this._authServices = []; + this._driverService = null; + ++ this._beginPromise = null; ++ this._redistributingRoles = false; ++ + this._messageQueue = []; + this._messageQueueTimeoutId = 0; + +@@ -143,6 +146,9 @@ var ShellUserVerifier = class { + this._cancellable?.cancel(); + this._cancellable = new Gio.Cancellable(); + ++ const {promise, resolve} = Promise.withResolvers(); ++ this._beginPromise = promise; ++ + try { + const proxies = await this._getUserVerifierProxies(userName, this._cancellable); + this._setUserVerifier(proxies.userVerifier); +@@ -157,6 +163,9 @@ var ShellUserVerifier = class { + logErrorUnlessCancelled(e); + } + ++ resolve(); ++ this._beginPromise = null; ++ + hold?.release(); + } + +@@ -458,7 +467,7 @@ var ShellUserVerifier = class { + '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(), ++ 'mechanisms-changed', () => this._onMechanismsChanged().catch(logError), + 'web-login', (_, args) => this.emit('web-login', args), + this); + }); +@@ -492,10 +501,12 @@ var ShellUserVerifier = class { + this._redistributingRoles = false; + } + +- _onMechanismsChanged() { ++ async _onMechanismsChanged() { + if (this._redistributingRoles) + return; + ++ await this._beginPromise; ++ + this._redistributeRoles(); + + // Collect mechanisms from all authServices in priority order, +-- +2.55.0 diff --git a/0001-Update-generated-stylesheets.patch b/0001-Update-generated-stylesheets.patch index 04fe5a3..ae470f7 100644 --- a/0001-Update-generated-stylesheets.patch +++ b/0001-Update-generated-stylesheets.patch @@ -3,12 +3,12 @@ From: =?UTF-8?q?Florian=20M=C3=BCllner?= Date: Tue, 8 Feb 2022 14:18:04 -0500 --- - data/theme/gnome-shell-high-contrast.css | 268 ++++++++++++++++++++++- - data/theme/gnome-shell.css | 268 ++++++++++++++++++++++- - 2 files changed, 524 insertions(+), 12 deletions(-) + data/theme/gnome-shell-high-contrast.css | 279 ++++++++++++++++++++++- + data/theme/gnome-shell.css | 279 ++++++++++++++++++++++- + 2 files changed, 544 insertions(+), 14 deletions(-) diff --git a/data/theme/gnome-shell-high-contrast.css b/data/theme/gnome-shell-high-contrast.css -index b73f407..585d6c4 100644 +index b73f407..5444be1 100644 --- a/data/theme/gnome-shell-high-contrast.css +++ b/data/theme/gnome-shell-high-contrast.css @@ -173,7 +173,7 @@ StEntry { @@ -52,21 +52,7 @@ index b73f407..585d6c4 100644 box-shadow: inset 0 0 0 100px rgba(255, 255, 255, 0.15); } #panel .panel-status-indicators-box, #panel .panel-status-menu-box { -@@ -1943,6 +1946,13 @@ StScrollBar { - border-radius: 8px; - padding: 6px; } - -+.qr-code { -+ border-radius: 4px; -+ border-width: 1em; -+ background-color: #eeeeec; -+ border-color: #eeeeec; -+ color: #282828; } -+ - /* Login Dialog */ - .login-dialog-banner-view { - padding-top: 24px; -@@ -2028,11 +2038,15 @@ StScrollBar { +@@ -2028,11 +2031,15 @@ StScrollBar { border-color: #184472; background-color: #184472; color: rgba(255, 255, 255, 0.7); } @@ -82,7 +68,7 @@ index b73f407..585d6c4 100644 .unlock-dialog .login-dialog-session-list-button { padding: 0; border-radius: 99px; -@@ -2040,18 +2054,51 @@ StScrollBar { +@@ -2040,18 +2047,59 @@ StScrollBar { height: 2.18em; border-color: #202020; background-color: #202020; } @@ -119,13 +105,21 @@ index b73f407..585d6c4 100644 + margin: 0.5em 20px; } + .login-dialog .login-dialog-prompt-entry, + .unlock-dialog .login-dialog-prompt-entry { -+ border-radius: 12px; -+ padding-right: 2.5em; } -+ .login-dialog .login-dialog-default-button-well, -+ .unlock-dialog .login-dialog-default-button-well { -+ margin-right: 0.4em; } ++ border-radius: 12px; } ++ .login-dialog .login-dialog-prompt-entry :ltr, ++ .unlock-dialog .login-dialog-prompt-entry :ltr { ++ padding-right: 2.5em; } ++ .login-dialog .login-dialog-prompt-entry :rtl, ++ .unlock-dialog .login-dialog-prompt-entry :rtl { ++ padding-left: 2.5em; } ++ .login-dialog .login-dialog-default-button-well :ltr, ++ .unlock-dialog .login-dialog-default-button-well :ltr { ++ margin-right: 0.5em; } ++ .login-dialog .login-dialog-default-button-well :rtl, ++ .unlock-dialog .login-dialog-default-button-well :rtl { ++ margin-left: 0.5em; } + -+.login-dialog-auth-menu-button-indicator-description { ++.login-dialog-spinner { + color: #eeeeec; } + +.login-dialog-bottom-button-group { @@ -134,7 +128,7 @@ index b73f407..585d6c4 100644 .login-dialog-logo-bin { padding: 24px 0px; } -@@ -2066,6 +2113,10 @@ StScrollBar { +@@ -2066,6 +2114,10 @@ StScrollBar { .login-dialog-message { text-align: center; } @@ -145,7 +139,7 @@ index b73f407..585d6c4 100644 .login-dialog-user-selection-box { padding: 100px 0px; } -@@ -2080,6 +2131,140 @@ StScrollBar { +@@ -2080,6 +2132,143 @@ StScrollBar { color: #a6a69b; padding-top: 1em; } @@ -159,46 +153,45 @@ index b73f407..585d6c4 100644 + .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-section-label { + font-size: 10pt; + font-weight: bold; -+ color: #eeeeec; } ++ color: #fff; } + .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-icon { -+ color: #eeeeec; ++ color: #fff; + icon-size: 1.09em; + 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: #eeeeec; ++ color: #fff; + font-size: 11pt; + font-weight: bold; } + .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-box-description { -+ color: #eeeeec; ++ color: #fff; + font-size: 10pt; } + +.login-dialog-auth-menu-button-indicator { + background-color: transparent !important; -+ border-width: 0; -+ margin-bottom: 32px; -+ margin-left: 32px; } ++ margin: 32px; } + .login-dialog-auth-menu-button-indicator .login-dialog-auth-menu-button-indicator-icons { + spacing: 18px; } + .login-dialog-auth-menu-button-indicator .login-dialog-auth-menu-button-indicator-icons .login-dialog-auth-menu-button-indicator-icon { + icon-size: 2em; } + .login-dialog-auth-menu-button-indicator .login-dialog-auth-menu-button-indicator-description { + font-size: 11pt; -+ margin-left: 12px; -+ min-width: 20em; } ++ font-weight: normal; ++ margin-left: 12px; } ++ ++.login-dialog-auth-menu-button-indicator-description { ++ color: #eeeeec; } + +.login-dialog-button-box { + height: 4em; } + +.login-dialog-auth-list-view { -+ -st-vfade-offset: 1em; ++ -st-vfade-offset: 3em; + max-height: 13em; } + +.login-dialog-auth-list { -+ spacing: 9px; -+ width: 23em; -+ margin-left: 20px; } ++ spacing: 9px; } + +.login-dialog .login-dialog-auth-list-title, +.login-dialog .login-dialog-auth-list-item { @@ -211,16 +204,18 @@ index b73f407..585d6c4 100644 + color: #ffffff; } + +.login-dialog .login-dialog-auth-list-title { -+ background-color: rgba(238, 238, 236, 0.03) !important; ++ background-color: rgba(238, 238, 236, 0.02) !important; + color: #eeeeec !important; -+ padding: 6px; + margin: 0.5em 20px; } + +.login-dialog .login-dialog-auth-list-item { + min-height: 3em; + padding: 9px; + margin-bottom: 4px; ++ margin-right: 4px; + background-color: rgba(238, 238, 236, 0.06); } ++ .login-dialog .login-dialog-auth-list-item.compact { ++ min-height: 1.3em; } + +.unlock-dialog .login-dialog-auth-list-item { + border: none; @@ -229,7 +224,8 @@ index b73f407..585d6c4 100644 + border-radius: 12px; + min-height: 3em; + padding: 9px; -+ margin-bottom: 4px; } ++ margin-bottom: 4px; ++ margin-right: 4px; } + .unlock-dialog .login-dialog-auth-list-item:hover, .unlock-dialog .login-dialog-auth-list-item:focus, .unlock-dialog .login-dialog-auth-list-item:selected { + background-color: rgba(238, 238, 236, 0.16); } + .unlock-dialog .login-dialog-auth-list-item:active { @@ -239,7 +235,8 @@ index b73f407..585d6c4 100644 + background-color: transparent !important; + border: none; + color: #eeeeec !important; -+ padding: 6px; } ++ padding: 0; ++ margin: 0; } + +.login-dialog-auth-list-title-label { + font-size: 13pt; @@ -251,7 +248,7 @@ index b73f407..585d6c4 100644 +.login-dialog-auth-list-item-subtitle { + font-size: 11pt; + text-align: center; -+ padding: 1.8px 0; } ++ padding: 1.8px 2em; } + +.login-dialog-auth-list-item-title { + color: #eeeeec; @@ -286,7 +283,16 @@ index b73f407..585d6c4 100644 .login-dialog-user-list-view { -st-vfade-offset: 1em; } -@@ -2145,6 +2330,77 @@ StScrollBar { +@@ -2135,7 +2324,7 @@ StScrollBar { + padding-top: 24px; + padding-bottom: 12px; + spacing: 12px; +- width: 23em; } ++ width: 27.6em; } + + .login-dialog-prompt-entry { + height: 1.5em; } +@@ -2145,6 +2334,75 @@ StScrollBar { font-size: 12pt; padding-top: 1em; } @@ -326,37 +332,35 @@ index b73f407..585d6c4 100644 + padding-right: 27px; + spacing: 1.75em; } + -+.web-login-button-label { ++.login-dialog .login-button, ++.login-dialog .web-login-prompt-button, ++.unlock-dialog .login-button, ++.unlock-dialog .web-login-prompt-button { + font-size: 13pt; + color: #eeeeec; -+ min-width: 12em; + text-align: center; + font-weight: bold; -+ padding-bottom: 0.1em; } -+ -+.login-dialog .web-login-intro-button, -+.login-dialog .web-login-prompt-button, -+.unlock-dialog .web-login-intro-button, -+.unlock-dialog .web-login-prompt-button { -+ border-radius: 32px !important; -+ padding: 9px 18px; ++ border-radius: 32px; + background-color: rgba(238, 238, 236, 0.06); } + -+.login-dialog .web-login-intro-button, -+.unlock-dialog .web-login-intro-button { -+ width: 10em; } ++.login-dialog .login-button, ++.unlock-dialog .login-button { ++ padding: 9px 18px; ++ margin: 0.2em 20px; ++ width: 8em; } + +.login-dialog .web-login-prompt-button, +.unlock-dialog .web-login-prompt-button { -+ width: 7em; -+ margin: 24px 8px; } ++ padding: 9px 18px; ++ margin: 24px 8px; ++ width: 7em; } + -+.login-dialog .web-login-intro-button:hover, .login-dialog .web-login-intro-button:focus, ++.login-dialog .login-button:hover, .login-dialog .login-button:focus, +.login-dialog .web-login-prompt-button:hover, +.login-dialog .web-login-prompt-button:focus { + background-color: #215d9c; } + -+.unlock-dialog .web-login-intro-button:hover, .unlock-dialog .web-login-intro-button:focus, ++.unlock-dialog .login-button:hover, .unlock-dialog .login-button:focus, +.unlock-dialog .web-login-prompt-button:hover, +.unlock-dialog .web-login-prompt-button:focus { + background-color: rgba(238, 238, 236, 0.16); } @@ -364,8 +368,22 @@ index b73f407..585d6c4 100644 /* Screen Shield */ .unlock-dialog-clock { color: white; +@@ -2210,6 +2468,13 @@ StScrollBar { + #unlockDialogNotifications StButton#vhandle:active, #unlockDialogNotifications StButton#hhandle:active { + background-color: rgba(33, 93, 156, 0.5); } + ++.qr-code { ++ border-radius: 4px; ++ border-width: 1em; ++ background-color: #eeeeec; ++ border-color: #eeeeec; ++ color: #282828; } ++ + stage { + -st-icon-style: symbolic; } + diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css -index f93819b..ff0c587 100644 +index f93819b..afe0767 100644 --- a/data/theme/gnome-shell.css +++ b/data/theme/gnome-shell.css @@ -173,7 +173,7 @@ StEntry { @@ -409,21 +427,7 @@ index f93819b..ff0c587 100644 box-shadow: inset 0 0 0 100px rgba(255, 255, 255, 0.15); } #panel .panel-status-indicators-box, #panel .panel-status-menu-box { -@@ -1943,6 +1946,13 @@ StScrollBar { - border-radius: 8px; - padding: 6px; } - -+.qr-code { -+ border-radius: 4px; -+ border-width: 1em; -+ background-color: #eeeeec; -+ border-color: #eeeeec; -+ color: #282828; } -+ - /* Login Dialog */ - .login-dialog-banner-view { - padding-top: 24px; -@@ -2028,11 +2038,15 @@ StScrollBar { +@@ -2028,11 +2031,15 @@ StScrollBar { border-color: #15539e; background-color: #15539e; color: rgba(255, 255, 255, 0.7); } @@ -439,7 +443,7 @@ index f93819b..ff0c587 100644 .unlock-dialog .login-dialog-session-list-button { padding: 0; border-radius: 99px; -@@ -2040,18 +2054,51 @@ StScrollBar { +@@ -2040,18 +2047,59 @@ StScrollBar { height: 2.18em; border-color: #202020; background-color: #202020; } @@ -476,13 +480,21 @@ index f93819b..ff0c587 100644 + margin: 0.5em 20px; } + .login-dialog .login-dialog-prompt-entry, + .unlock-dialog .login-dialog-prompt-entry { -+ border-radius: 12px; -+ padding-right: 2.5em; } -+ .login-dialog .login-dialog-default-button-well, -+ .unlock-dialog .login-dialog-default-button-well { -+ margin-right: 0.4em; } ++ border-radius: 12px; } ++ .login-dialog .login-dialog-prompt-entry :ltr, ++ .unlock-dialog .login-dialog-prompt-entry :ltr { ++ padding-right: 2.5em; } ++ .login-dialog .login-dialog-prompt-entry :rtl, ++ .unlock-dialog .login-dialog-prompt-entry :rtl { ++ padding-left: 2.5em; } ++ .login-dialog .login-dialog-default-button-well :ltr, ++ .unlock-dialog .login-dialog-default-button-well :ltr { ++ margin-right: 0.5em; } ++ .login-dialog .login-dialog-default-button-well :rtl, ++ .unlock-dialog .login-dialog-default-button-well :rtl { ++ margin-left: 0.5em; } + -+.login-dialog-auth-menu-button-indicator-description { ++.login-dialog-spinner { + color: #eeeeec; } + +.login-dialog-bottom-button-group { @@ -491,7 +503,7 @@ index f93819b..ff0c587 100644 .login-dialog-logo-bin { padding: 24px 0px; } -@@ -2066,6 +2113,10 @@ StScrollBar { +@@ -2066,6 +2114,10 @@ StScrollBar { .login-dialog-message { text-align: center; } @@ -502,7 +514,7 @@ index f93819b..ff0c587 100644 .login-dialog-user-selection-box { padding: 100px 0px; } -@@ -2080,6 +2131,140 @@ StScrollBar { +@@ -2080,6 +2132,143 @@ StScrollBar { color: #a6a69b; padding-top: 1em; } @@ -533,29 +545,28 @@ index f93819b..ff0c587 100644 + +.login-dialog-auth-menu-button-indicator { + background-color: transparent !important; -+ border-width: 0; -+ margin-bottom: 32px; -+ margin-left: 32px; } ++ margin: 32px; } + .login-dialog-auth-menu-button-indicator .login-dialog-auth-menu-button-indicator-icons { + spacing: 18px; } + .login-dialog-auth-menu-button-indicator .login-dialog-auth-menu-button-indicator-icons .login-dialog-auth-menu-button-indicator-icon { + icon-size: 2em; } + .login-dialog-auth-menu-button-indicator .login-dialog-auth-menu-button-indicator-description { + font-size: 11pt; -+ margin-left: 12px; -+ min-width: 20em; } ++ font-weight: normal; ++ margin-left: 12px; } ++ ++.login-dialog-auth-menu-button-indicator-description { ++ color: #eeeeec; } + +.login-dialog-button-box { + height: 4em; } + +.login-dialog-auth-list-view { -+ -st-vfade-offset: 1em; ++ -st-vfade-offset: 3em; + max-height: 13em; } + +.login-dialog-auth-list { -+ spacing: 9px; -+ width: 23em; -+ margin-left: 20px; } ++ spacing: 9px; } + +.login-dialog .login-dialog-auth-list-title, +.login-dialog .login-dialog-auth-list-item { @@ -568,16 +579,18 @@ index f93819b..ff0c587 100644 + color: #fff; } + +.login-dialog .login-dialog-auth-list-title { -+ background-color: rgba(238, 238, 236, 0.03) !important; ++ background-color: rgba(238, 238, 236, 0.02) !important; + color: #eeeeec !important; -+ padding: 6px; + margin: 0.5em 20px; } + +.login-dialog .login-dialog-auth-list-item { + min-height: 3em; + padding: 9px; + margin-bottom: 4px; ++ margin-right: 4px; + background-color: rgba(238, 238, 236, 0.06); } ++ .login-dialog .login-dialog-auth-list-item.compact { ++ min-height: 1.3em; } + +.unlock-dialog .login-dialog-auth-list-item { + border: none; @@ -586,7 +599,8 @@ index f93819b..ff0c587 100644 + border-radius: 12px; + min-height: 3em; + padding: 9px; -+ margin-bottom: 4px; } ++ margin-bottom: 4px; ++ margin-right: 4px; } + .unlock-dialog .login-dialog-auth-list-item:hover, .unlock-dialog .login-dialog-auth-list-item:focus, .unlock-dialog .login-dialog-auth-list-item:selected { + background-color: rgba(238, 238, 236, 0.16); } + .unlock-dialog .login-dialog-auth-list-item:active { @@ -596,7 +610,8 @@ index f93819b..ff0c587 100644 + background-color: transparent !important; + border: none; + color: #eeeeec !important; -+ padding: 6px; } ++ padding: 0; ++ margin: 0; } + +.login-dialog-auth-list-title-label { + font-size: 13pt; @@ -608,7 +623,7 @@ index f93819b..ff0c587 100644 +.login-dialog-auth-list-item-subtitle { + font-size: 11pt; + text-align: center; -+ padding: 1.8px 0; } ++ padding: 1.8px 2em; } + +.login-dialog-auth-list-item-title { + color: #eeeeec; @@ -643,7 +658,16 @@ index f93819b..ff0c587 100644 .login-dialog-user-list-view { -st-vfade-offset: 1em; } -@@ -2145,6 +2330,77 @@ StScrollBar { +@@ -2135,7 +2324,7 @@ StScrollBar { + padding-top: 24px; + padding-bottom: 12px; + spacing: 12px; +- width: 23em; } ++ width: 27.6em; } + + .login-dialog-prompt-entry { + height: 1.5em; } +@@ -2145,6 +2334,75 @@ StScrollBar { font-size: 12pt; padding-top: 1em; } @@ -683,37 +707,35 @@ index f93819b..ff0c587 100644 + padding-right: 27px; + spacing: 1.75em; } + -+.web-login-button-label { ++.login-dialog .login-button, ++.login-dialog .web-login-prompt-button, ++.unlock-dialog .login-button, ++.unlock-dialog .web-login-prompt-button { + font-size: 13pt; + color: #eeeeec; -+ min-width: 12em; + text-align: center; + font-weight: bold; -+ padding-bottom: 0.1em; } -+ -+.login-dialog .web-login-intro-button, -+.login-dialog .web-login-prompt-button, -+.unlock-dialog .web-login-intro-button, -+.unlock-dialog .web-login-prompt-button { -+ border-radius: 32px !important; -+ padding: 9px 18px; ++ border-radius: 32px; + background-color: rgba(238, 238, 236, 0.06); } + -+.login-dialog .web-login-intro-button, -+.unlock-dialog .web-login-intro-button { -+ width: 10em; } ++.login-dialog .login-button, ++.unlock-dialog .login-button { ++ padding: 9px 18px; ++ margin: 0.2em 20px; ++ width: 8em; } + +.login-dialog .web-login-prompt-button, +.unlock-dialog .web-login-prompt-button { -+ width: 7em; -+ margin: 24px 8px; } ++ padding: 9px 18px; ++ margin: 24px 8px; ++ width: 7em; } + -+.login-dialog .web-login-intro-button:hover, .login-dialog .web-login-intro-button:focus, ++.login-dialog .login-button:hover, .login-dialog .login-button:focus, +.login-dialog .web-login-prompt-button:hover, +.login-dialog .web-login-prompt-button:focus { + background-color: #1b6acb; } + -+.unlock-dialog .web-login-intro-button:hover, .unlock-dialog .web-login-intro-button:focus, ++.unlock-dialog .login-button:hover, .unlock-dialog .login-button:focus, +.unlock-dialog .web-login-prompt-button:hover, +.unlock-dialog .web-login-prompt-button:focus { + background-color: rgba(238, 238, 236, 0.16); } @@ -721,6 +743,17 @@ index f93819b..ff0c587 100644 /* Screen Shield */ .unlock-dialog-clock { color: white; +@@ -2209,3 +2467,10 @@ StScrollBar { + background-color: rgba(53, 53, 53, 0.5); } + #unlockDialogNotifications StButton#vhandle:active, #unlockDialogNotifications StButton#hhandle:active { + background-color: rgba(27, 106, 203, 0.5); } ++ ++.qr-code { ++ border-radius: 4px; ++ border-width: 1em; ++ background-color: #eeeeec; ++ border-color: #eeeeec; ++ color: #282828; } -- -2.51.0 +2.55.0 diff --git a/gnome-shell.spec b/gnome-shell.spec index 2262106..eb1e7fd 100644 --- a/gnome-shell.spec +++ b/gnome-shell.spec @@ -8,7 +8,7 @@ Name: gnome-shell Version: 40.10 -Release: 36%{?dist} +Release: 37%{?dist} Summary: Window management and application launching for GNOME License: GPLv2+ @@ -315,6 +315,11 @@ desktop-file-validate %{buildroot}%{_datadir}/applications/evolution-calendar.de %endif %changelog +* Tue Aug 11 2026 Joan Torres Lopez - 40.10-37 +- Update passwordless GDM backport patch series + Add required pre-changes and update post-changes + Resolves: RHEL-236554 + * Mon May 25 2026 Joan Torres Lopez - 40.10-36 - Update fix of registering session Registering the display is needed too diff --git a/post-changes-for-passwordless-gdm-backport.patch b/post-changes-for-passwordless-gdm-backport.patch index d1b7103..cc35c66 100644 --- a/post-changes-for-passwordless-gdm-backport.patch +++ b/post-changes-for-passwordless-gdm-backport.patch @@ -1,30 +1,17 @@ -From 183d2cce2a34a0d6fca990c2264bae60cdfc5de4 Mon Sep 17 00:00:00 2001 +From 46e8c769240407b70d4e233109ea3888aad3209b Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Wed, 25 Mar 2026 18:58:29 +0100 -Subject: [PATCH 01/30] gdm: Update styles from login-lock to login-dialog +Subject: [PATCH 01/47] gdm: Update styles from login-lock to login-dialog login-lock is used in newer versions, but in this version login-dialog is used. --- - data/theme/gnome-shell-sass/_widgets.scss | 1 + - .../widgets/_login-dialog.scss | 307 +++++++++++++++++- + .../widgets/_login-dialog.scss | 315 +++++++++++++++++- .../gnome-shell-sass/widgets/_qr-code.scss | 4 +- - 3 files changed, 296 insertions(+), 16 deletions(-) + 2 files changed, 302 insertions(+), 17 deletions(-) -diff --git a/data/theme/gnome-shell-sass/_widgets.scss b/data/theme/gnome-shell-sass/_widgets.scss -index a8d0aa9f4d..1651d9ca3b 100644 ---- a/data/theme/gnome-shell-sass/_widgets.scss -+++ b/data/theme/gnome-shell-sass/_widgets.scss -@@ -46,6 +46,7 @@ - @import 'widgets/tiled-previews'; - @import 'widgets/keyboard'; - @import 'widgets/looking-glass'; -+@import 'widgets/qr-code'; - // Lock / login screens - @import 'widgets/login-dialog'; - @import 'widgets/screen-shield'; diff --git a/data/theme/gnome-shell-sass/widgets/_login-dialog.scss b/data/theme/gnome-shell-sass/widgets/_login-dialog.scss -index 307a751628..f3aeed4bf3 100644 +index 307a75162..883a0e14c 100644 --- a/data/theme/gnome-shell-sass/widgets/_login-dialog.scss +++ b/data/theme/gnome-shell-sass/widgets/_login-dialog.scss @@ -1,5 +1,7 @@ @@ -46,7 +33,7 @@ index 307a751628..f3aeed4bf3 100644 .login-dialog-session-list-button { padding: 0; border-radius: 99px; -@@ -83,10 +87,40 @@ +@@ -83,10 +87,43 @@ StIcon { icon-size: $base_icon_size; } } @@ -78,16 +65,19 @@ index 307a751628..f3aeed4bf3 100644 + + .login-dialog-prompt-entry { + border-radius: $base_border_radius * 1.5; -+ padding-right: 2.5em; // Make room for button-well inside entry ++ // Make room for button-well inside entry ++ :ltr { padding-right: 2.5em; } ++ :rtl { padding-left: 2.5em; } + } + + .login-dialog-default-button-well { -+ margin-right: 0.4em; ++ :ltr { margin-right: 0.5em; } ++ :rtl { margin-left: 0.5em; } + } } .login-dialog-bottom-button-group { -@@ -118,30 +152,185 @@ +@@ -118,30 +155,191 @@ padding-top: 1em; } @@ -134,9 +124,7 @@ index 307a751628..f3aeed4bf3 100644 + +.login-dialog-auth-menu-button-indicator { + background-color: transparent !important; -+ border-width: 0; -+ margin-bottom: 32px; -+ margin-left: 32px; ++ margin: 32px; + + .login-dialog-auth-menu-button-indicator-icons { + spacing: $base_padding * 3; @@ -148,17 +136,21 @@ index 307a751628..f3aeed4bf3 100644 + + .login-dialog-auth-menu-button-indicator-description { + @include fontsize($base_font_size); ++ font-weight: normal; + margin-left: $base_padding * 2; -+ min-width: 20em; + } +} + ++.login-dialog-auth-menu-button-indicator-description { ++ color: $osd_fg_color; ++} ++ +.login-dialog-button-box { + height: 4em; +} + +.login-dialog-auth-list-view { -+ -st-vfade-offset: 1em; ++ -st-vfade-offset: 3em; + max-height: 13em; +} + @@ -166,8 +158,6 @@ index 307a751628..f3aeed4bf3 100644 - spacing: 6px; - margin-left: 2em; + spacing: $base_padding * 1.5; -+ width: 23em; -+ margin-left: $base_margin * 5; } -.login-dialog-auth-list-title { @@ -181,9 +171,8 @@ index 307a751628..f3aeed4bf3 100644 + } + + .login-dialog-auth-list-title { -+ background-color: transparentize($_gdm_fg, .97) !important; ++ background-color: transparentize($_gdm_fg, .98) !important; + color: $_gdm_fg !important; -+ padding: $base_padding; + margin: 0.5em $base_margin * 5; + } + @@ -191,7 +180,12 @@ index 307a751628..f3aeed4bf3 100644 + min-height: 3em; + padding: $base_padding * 1.5; + margin-bottom: $base_margin; ++ margin-right: $base_margin; + background-color: transparentize($_gdm_fg, .94); ++ ++ &.compact { ++ min-height: 1.3em; ++ } + } } @@ -211,13 +205,15 @@ index 307a751628..f3aeed4bf3 100644 + min-height: 3em; + padding: $base_padding * 1.5; + margin-bottom: $base_margin; ++ margin-right: $base_margin; + } + + .login-dialog-auth-list-title { + background-color: transparent !important; + border: none; + color: $osd_fg_color !important; -+ padding: $base_padding; ++ padding: 0; ++ margin: 0; + } } @@ -236,7 +232,7 @@ index 307a751628..f3aeed4bf3 100644 +.login-dialog-auth-list-item-subtitle { + @include fontsize($base_font_size); + text-align: center; -+ padding: $base_padding * 0.3 0; ++ padding: $base_padding * 0.3 2em; +} + +.login-dialog-auth-list-item-title { @@ -287,7 +283,16 @@ index 307a751628..f3aeed4bf3 100644 } .login-dialog-user-list-view { -st-vfade-offset: 1em; } -@@ -207,3 +396,93 @@ +@@ -195,7 +393,7 @@ + padding-top: 24px; + padding-bottom: 12px; + spacing: $base_spacing * 2; +- width: 23em; ++ width: 23em * 1.2; + } + + .login-dialog-prompt-entry { +@@ -207,3 +405,90 @@ @include fontsize($base_font_size + 1); padding-top: 1em; } @@ -336,36 +341,33 @@ index 307a751628..f3aeed4bf3 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; -+ padding-bottom: 0.1em; -+} -+ +.login-dialog, +.unlock-dialog { -+ .web-login-intro-button, ++ .login-button, + .web-login-prompt-button { -+ border-radius: $base_border_radius * 4 !important; -+ padding: $base_padding * 1.5 $base_padding * 3; ++ @include fontsize($base_font_size + 2); ++ color: $_gdm_fg; ++ text-align: center; ++ font-weight: bold; ++ border-radius: $base_border_radius * 4; + background-color: transparentize($_gdm_fg, .94); + } + -+ .web-login-intro-button { -+ width: 10em; ++ .login-button { ++ padding: $base_padding * 1.5 $base_padding * 3; ++ margin: 0.2em $base_margin * 5; ++ width: 8em; + } + + .web-login-prompt-button { -+ width: 7em; ++ padding: $base_padding * 1.5 $base_padding * 3; + margin: $base_margin * 6 $base_margin * 2; ++ width: 7em; + } +} + +.login-dialog { -+ .web-login-intro-button, ++ .login-button, + .web-login-prompt-button { + &:hover, &:focus { + background-color: $selected_bg_color; @@ -374,7 +376,7 @@ index 307a751628..f3aeed4bf3 100644 +} + +.unlock-dialog { -+ .web-login-intro-button, ++ .login-button, + .web-login-prompt-button { + &:hover, &:focus { + background-color: transparentize($osd_fg_color, .84); @@ -382,7 +384,7 @@ index 307a751628..f3aeed4bf3 100644 + } +} diff --git a/data/theme/gnome-shell-sass/widgets/_qr-code.scss b/data/theme/gnome-shell-sass/widgets/_qr-code.scss -index da04425bd6..0d733453f2 100644 +index da04425bd..0d733453f 100644 --- a/data/theme/gnome-shell-sass/widgets/_qr-code.scss +++ b/data/theme/gnome-shell-sass/widgets/_qr-code.scss @@ -7,8 +7,8 @@ @@ -397,36 +399,1298 @@ index da04425bd6..0d733453f2 100644 } } -- -2.51.0 +2.55.0 -From a6a9baab3e92e3adb9172227051e83c4d145e1df Mon Sep 17 00:00:00 2001 +From 40e819ffdfd40cac4276ef00f4211ef522071f59 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez -Date: Fri, 26 Dec 2025 14:52:58 +0100 -Subject: [PATCH 02/30] gdm: Fix importing using the old gjs style +Date: Tue, 2 Jun 2026 12:07:45 +0200 +Subject: [PATCH 02/47] Use Object.keys(this._sections) instead of + this._sections.keys() --- - js/gdm/authMenuButton.js | 13 +++++-------- - js/gdm/authServices.js | 18 +++++++----------- - js/gdm/authServicesLegacy.js | 18 ++++++++---------- - js/gdm/authServicesSSSDSwitchable.js | 10 ++++------ - js/gdm/webLogin.js | 14 +++++++------- - js/misc/fingerprintManager.js | 10 ++++------ - js/misc/passkeyDeviceManager.js | 3 +-- - js/ui/qrCode.js | 10 +++------- - 8 files changed, 39 insertions(+), 57 deletions(-) + 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 9b7cebf6d8..f7f458dec8 100644 +index 5aaa45fd3..e2c516585 100644 --- a/js/gdm/authMenuButton.js +++ b/js/gdm/authMenuButton.js -@@ -1,4 +1,5 @@ - // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- -+/* exported AuthMenuButton, AuthMenuButtonIndicator */ - /* - * Copyright 2024 Red Hat, Inc - * -@@ -43,15 +44,11 @@ +@@ -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 f18539cd8dc5b458ea6775b0a78166144cdea20b Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Tue, 2 Jun 2026 12:52:09 +0200 +Subject: [PATCH 03/47] 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 5bc11c8e8bcca29d306562891f33aeedd72b31ec Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Tue, 9 Jun 2026 09:57:09 +0200 +Subject: [PATCH 04/47] 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 c1e186480..01d2b463a 100644 +--- a/js/gdm/userVerifier.js ++++ b/js/gdm/userVerifier.js +@@ -146,7 +146,11 @@ var ShellUserVerifier = class { + this._cancellable?.cancel(); + this._cancellable = new Gio.Cancellable(); + +- const {promise, resolve} = Promise.withResolvers(); ++ let resolve, reject; ++ const promise = new Promise((res, rej) => { ++ resolve = res; ++ reject = rej; ++ }); + this._beginPromise = promise; + + try { +-- +2.55.0 + + +From b69306bd85fbbae29b901a134a2aba65691e770f Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Thu, 6 Aug 2026 23:58:46 +0200 +Subject: [PATCH 05/47] 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 ac2165e46..551442372 100644 +--- a/js/gdm/authList.js ++++ b/js/gdm/authList.js +@@ -65,7 +65,7 @@ var ItemIcon = class 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 ffc73c9dc..d7e81a928 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -284,7 +284,7 @@ var 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 699b45469975a14f1a4cf5aaf378f2af2f1e256f Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Fri, 7 Aug 2026 11:42:37 +0200 +Subject: [PATCH 06/47] style: Fix color styles to adapt when on light-mode + (classic-mode) + +--- + data/theme/gnome-shell-sass/widgets/_login-dialog.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-dialog.scss b/data/theme/gnome-shell-sass/widgets/_login-dialog.scss +index 883a0e14c..02610878e 100644 +--- a/data/theme/gnome-shell-sass/widgets/_login-dialog.scss ++++ b/data/theme/gnome-shell-sass/widgets/_login-dialog.scss +@@ -169,12 +169,12 @@ $_gdm_fg: $osd_fg_color; + &-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: $base_icon_size; + margin-right: $base_margin * 2; + } +@@ -183,13 +183,13 @@ $_gdm_fg: $osd_fg_color; + 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 0d733453f..e156d2235 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: $osd_fg_color; + border-color: $osd_fg_color; +-- +2.55.0 + + +From a67c9ba7cbc390f149d3858d6d189228add65ef1 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Wed, 12 Aug 2026 12:37:41 +0200 +Subject: [PATCH 07/47] gdm: Restore util.js and misc/smartcardManager.js as + compat shims + +Several symbols moved out of util.js into userVerifier.js and +settings.js, and smartcardManager.js moved from misc/ to gdm/. +Extensions importing from the old locations would break. + +Re-export the moved symbols from util.js, and add back +misc/smartcardManager.js as a thin re-export of the new gdm/ +location, to keep the old import paths and names working. +--- + js/gdm/util.js | 21 +++++++++++++++++++++ + js/js-resources.gresource.xml | 1 + + js/misc/smartcardManager.js | 3 +++ + 3 files changed, 25 insertions(+) + create mode 100644 js/misc/smartcardManager.js + +diff --git a/js/gdm/util.js b/js/gdm/util.js +index 10f9131d9..d9a7e2239 100644 +--- a/js/gdm/util.js ++++ b/js/gdm/util.js +@@ -3,6 +3,27 @@ import Clutter from 'gi://Clutter'; + import * as Batch from './batch.js'; + import * as Main from '../ui/main.js'; + ++// Re-exported for backward compatibility: these symbols used to live in ++// this file before being split into userVerifier.js and settings.js. ++export {MessageType, ShellUserVerifier} from './userVerifier.js'; ++export { ++ LOGIN_SCREEN_SCHEMA, ++ PASSWORD_AUTHENTICATION_KEY, ++ FINGERPRINT_AUTHENTICATION_KEY, ++ SMARTCARD_AUTHENTICATION_KEY, ++ BANNER_MESSAGE_KEY, ++ BANNER_MESSAGE_SOURCE_KEY, ++ BANNER_MESSAGE_TEXT_KEY, ++ BANNER_MESSAGE_PATH_KEY, ++ ALLOWED_FAILURES_KEY, ++ LOGO_KEY, ++ DISABLE_USER_LIST_KEY, ++} from './settings.js'; ++ ++export const PASSWORD_SERVICE_NAME = 'gdm-password'; ++export const FINGERPRINT_SERVICE_NAME = 'gdm-fingerprint'; ++export const SMARTCARD_SERVICE_NAME = 'gdm-smartcard'; ++ + export const CLONE_FADE_ANIMATION_TIME = 250; + + /** +diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml +index cf70dd2fd..a4a0a958d 100644 +--- a/js/js-resources.gresource.xml ++++ b/js/js-resources.gresource.xml +@@ -37,6 +37,7 @@ + misc/params.js + misc/parentalControlsManager.js + misc/permissionStore.js ++ misc/smartcardManager.js + misc/systemActions.js + misc/util.js + misc/weather.js +diff --git a/js/misc/smartcardManager.js b/js/misc/smartcardManager.js +new file mode 100644 +index 000000000..df62ec048 +--- /dev/null ++++ b/js/misc/smartcardManager.js +@@ -0,0 +1,3 @@ ++// Re-exported for backward compatibility: this module used to live here ++// before being moved to gdm/smartcardManager.js. ++export {getSmartcardManager} from '../gdm/smartcardManager.js'; +-- +2.55.0 + + +From a066d3717ec671be72a955eb99ecb8438ed3a2bf Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Wed, 12 Aug 2026 12:37:50 +0200 +Subject: [PATCH 08/47] authPrompt: Restore BeginRequestType as alias for + ResetType + +ResetType was renamed from BeginRequestType. Keep the old name +exported as an alias so extensions referencing +AuthPrompt.BeginRequestType keep working. +--- + js/gdm/authPrompt.js | 3 +++ + 1 file changed, 3 insertions(+) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index d7e81a928..05ad13931 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -42,6 +42,9 @@ var ResetType = { + REUSE_USERNAME: 2, + }; + ++// Kept for backward compatibility: ResetType used to be named BeginRequestType. ++export const BeginRequestType = ResetType; ++ + var AuthPrompt = GObject.registerClass({ + Signals: { + 'cancelled': {}, +-- +2.55.0 + + +From fe14d6dcd05aa3da5308703b3663d6155f0e960f Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Wed, 12 Aug 2026 12:38:25 +0200 +Subject: [PATCH 09/47] Revert "gdm/authServices: Use per-request callbacks to + handle services queries" + +This reverts commit 929f864f1135a4225c09857ae3655d16de6c9251. + +The per-request callback pattern removed ShellUserVerifier.answerQuery() +and selectChoice() as public methods, breaking extensions that call them +directly (e.g. ShellUserVerifier subclasses used by external IdP +integrations). Restore the original answerQuery()/selectChoice() API so +those extensions keep working, at the cost of the per-request context +safety the callback pattern added. +--- + 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, 79 insertions(+), 117 deletions(-) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index 05ad13931..6f586b834 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -183,7 +183,20 @@ var AuthPrompt = GObject.registerClass({ + + this._authList = new AuthList.AuthList(); + this._authList.hide(); +- this._authListActivateId = 0; ++ 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.add_child(this._authList); + + this._authListTitle = new St.Bin({ +@@ -293,7 +306,6 @@ var 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(); +@@ -372,7 +384,7 @@ var AuthPrompt = GObject.registerClass({ + this.startSpinning({animate: true}); + + if (this._queryingService) { +- this._completePendingCallback(this._entry.text); ++ this._userVerifier.answerQuery(this._queryingService, this._entry.text); + } else { + this._preemptiveAnswer = this._entry.text; + +@@ -410,37 +422,17 @@ var AuthPrompt = GObject.registerClass({ + this._capsLockWarningLabel.visible = 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}) { ++ _onAskQuestion({serviceName, question, secret}) { + if (this._queryingService) + this.clear(); + + this._queryingService = serviceName; + this.promptStep++; + +- this._setPendingCallback(answerHandler); +- + const preemptiveAnswer = this._preemptiveAnswer; + this._clearPreemptiveState(); + if (preemptiveAnswer) { +- this._completePendingCallback(preemptiveAnswer); ++ this._userVerifier.answerQuery(this._queryingService, preemptiveAnswer); + return; + } + +@@ -457,7 +449,7 @@ var AuthPrompt = GObject.registerClass({ + this.emit('prompted'); + } + +- _onShowChoiceList({serviceName, promptMessage, choiceList, choiceHandler}) { ++ _onShowChoiceList({serviceName, promptMessage, choiceList}) { + if (this._queryingService) + this.clear(); + +@@ -466,9 +458,6 @@ var AuthPrompt = GObject.registerClass({ + + this._clearPreemptiveState(); + +- this._connectAuthListActivate(); +- this._setPendingCallback(choiceHandler); +- + this.setChoiceList(promptMessage, choiceList); + this.updateSensitivity({sensitive: true}); + this.emit('prompted'); +@@ -512,7 +501,10 @@ var AuthPrompt = GObject.registerClass({ + + this._clearPreemptiveState(); + +- this._setPendingCallback(callback); ++ const authButtonClickedId = this._authButton.connect('clicked', () => { ++ this._authButton.disconnect(authButtonClickedId); ++ callback(); ++ }); + + this._authButton.set_label(label); + +@@ -687,10 +679,8 @@ var 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; +@@ -714,34 +704,6 @@ var 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 e5cf21c96..e5c60e1d7 100644 +--- a/js/gdm/authServices.js ++++ b/js/gdm/authServices.js +@@ -81,7 +81,6 @@ 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]}, + /** +@@ -93,7 +92,6 @@ 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]}, + /** +@@ -167,6 +165,14 @@ 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(); +@@ -483,6 +489,10 @@ 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 0e5c525de..f257b55fc 100644 +--- a/js/gdm/authServicesLegacy.js ++++ b/js/gdm/authServicesLegacy.js +@@ -77,6 +77,14 @@ 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; +@@ -246,11 +254,7 @@ export class AuthServicesLegacy extends AuthServices { + if (serviceName !== this._selectedMechanism?.serviceName) + return; + +- this.emit('ask-question', { +- serviceName, +- question, +- answerHandler: answer => this._handleAnswerQuery(serviceName, answer), +- }); ++ this.emit('ask-question', {serviceName, question}); + } + + _handleOnSecretInfoQuery(serviceName, secretQuestion) { +@@ -262,7 +266,7 @@ export class AuthServicesLegacy extends AuthServices { + token = this._credentialManagers[serviceName].token; + + if (token) { +- this._handleAnswerQuery(serviceName, token); ++ this.answerQuery(serviceName, token); + return; + } + +@@ -270,7 +274,6 @@ export class AuthServicesLegacy extends AuthServices { + serviceName, + question: secretQuestion, + secret: true, +- answerHandler: answer => this._handleAnswerQuery(serviceName, answer), + }); + } + +@@ -346,17 +349,7 @@ 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, +- choiceHandler: key => { +- if (serviceName !== this._selectedMechanism?.serviceName) +- return; +- this._userVerifierChoiceList.call_select_choice( +- serviceName, key, this._cancellable).catch(logErrorUnlessCancelled); +- }, +- }); ++ this.emit('show-choice-list', {serviceName, promptMessage, choiceList}); + } + + _handleGetCredentialManagerServices() { +diff --git a/js/gdm/authServicesSSSDSwitchable.js b/js/gdm/authServicesSSSDSwitchable.js +index 0271d98f2..3a81b99c2 100644 +--- a/js/gdm/authServicesSSSDSwitchable.js ++++ b/js/gdm/authServicesSSSDSwitchable.js +@@ -74,6 +74,23 @@ 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; +@@ -349,7 +366,6 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + serviceName, + question: secretQuestion, + secret: true, +- answerHandler: answer => this._handleAnswerQuery(serviceName, answer), + }); + } + } +@@ -437,12 +453,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + const {serviceName, prompt} = this._selectedMechanism; + + this._promptStatus = PromptStatus.PASSWORD_PROMPT; +- this.emit('ask-question', { +- serviceName, +- question: prompt, +- secret: true, +- answerHandler: answer => this._handleAnswerQuery(serviceName, answer), +- }); ++ this.emit('ask-question', {serviceName, question: prompt, secret: true}); + } + + _startSmartcardLogin() { +@@ -455,7 +466,6 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + serviceName, + question: certificates[0].pinPrompt, + secret: true, +- answerHandler: answer => this._handleAnswerQuery(serviceName, answer), + }); + return; + } +@@ -469,27 +479,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + : _('Select Identity'); + + this._promptStatus = PromptStatus.CERT_LIST_PROMPT; +- 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), +- }); +- } +- }, +- }); ++ this.emit('show-choice-list', {serviceName, promptMessage, choiceList}); + } + + _parseCertInstruction(certInstruction) { +@@ -524,12 +514,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + } + + this._promptStatus = PromptStatus.PIN_PROMPT; +- this.emit('ask-question', { +- serviceName, +- question: pinPrompt, +- secret: true, +- answerHandler: answer => this._handleAnswerQuery(serviceName, answer), +- }); ++ 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); +diff --git a/js/gdm/userVerifier.js b/js/gdm/userVerifier.js +index 01d2b463a..cd7add9c9 100644 +--- a/js/gdm/userVerifier.js ++++ b/js/gdm/userVerifier.js +@@ -250,6 +250,18 @@ var ShellUserVerifier = class { + 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)); + } +@@ -529,14 +541,14 @@ var ShellUserVerifier = class { + + 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 56b65f8e24f7032889ec04c9882819f05b835f0f Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Wed, 12 Aug 2026 12:40:53 +0200 +Subject: [PATCH 10/47] gdm: Restore positional signal signatures on + ShellUserVerifier + +Signals on ShellUserVerifier ('ask-question', 'show-message', +'show-choice-list', 'verification-failed', 'reset') were changed to +pass a single object argument instead of the original positional +arguments, breaking extensions that connect to them with the old +handler signatures (e.g. (verifier, serviceName, question, secret)). + +Restore the original positional argument order. Where the object +carried genuinely new information (like the show-message wiggle +resolver), append it as a trailing positional argument, which old +handlers simply ignore. +--- + js/gdm/authPrompt.js | 12 ++++++++---- + js/gdm/userVerifier.js | 20 +++++++++++--------- + 2 files changed, 19 insertions(+), 13 deletions(-) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index 6f586b834..e874e459d 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -95,13 +95,17 @@ var AuthPrompt = GObject.registerClass({ + this._userVerifier = new UserVerifier.ShellUserVerifier(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), ++ 'ask-question', (_, serviceName, question, secret) => ++ this._onAskQuestion({serviceName, question, secret}), ++ 'show-message', (_, serviceName, message, type, shouldWiggle) => ++ this._onShowMessage({message, type, shouldWiggle}), ++ 'show-choice-list', (_, serviceName, promptMessage, choiceList) => ++ this._onShowChoiceList({serviceName, promptMessage, choiceList}), + '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-failed', (_, serviceName, canRetry) => ++ this._onVerificationFailed({serviceName, canRetry}), + 'verification-complete', () => this._onVerificationComplete(), + 'reset', (_, args) => this._onReset(args), + this); +diff --git a/js/gdm/userVerifier.js b/js/gdm/userVerifier.js +index cd7add9c9..b8fb22c24 100644 +--- a/js/gdm/userVerifier.js ++++ b/js/gdm/userVerifier.js +@@ -311,11 +311,11 @@ var ShellUserVerifier = class { + const message = this.currentMessage; + + delete this._currentMessageExtraInterval; +- this.emit('show-message', { +- message: message.text, +- type: message.type, +- shouldWiggle: message.wiggle, +- }); ++ this.emit('show-message', ++ message.serviceName, ++ message.text, ++ message.type, ++ message.wiggle); + + this._messageQueueTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, + message.interval + (this._currentMessageExtraInterval | 0), () => { +@@ -368,7 +368,7 @@ var ShellUserVerifier = class { + this._messageQueueTimeoutId = 0; + } + +- this.emit('show-message', {type: MessageType.NONE}); ++ this.emit('show-message', null, null, MessageType.NONE); + } + + _reportInitError(initError) { +@@ -473,7 +473,8 @@ var ShellUserVerifier = class { + _connectAuthServices() { + this._authServices.forEach(authServices => { + authServices.connectObject( +- 'ask-question', (_, args) => this.emit('ask-question', args), ++ 'ask-question', (_, args) => ++ this.emit('ask-question', args.serviceName, args.question, args.secret ?? false), + 'queue-message', (_, args) => this._queueMessage(args), + 'queue-priority-message', (_, args) => this._queuePriorityMessage(args), + 'wait-pending-messages', (_, args) => this._waitPendingMessages(args), +@@ -481,7 +482,8 @@ var ShellUserVerifier = class { + '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-choice-list', (_, args) => ++ this.emit('show-choice-list', args.serviceName, args.promptMessage, args.choiceList), + 'show-button', (_, args) => this.emit('show-button', args), + 'mechanisms-changed', () => this._onMechanismsChanged().catch(logError), + 'web-login', (_, args) => this.emit('web-login', args), +@@ -491,7 +493,7 @@ var ShellUserVerifier = class { + + _verificationFailed({serviceName, canRetry}) { + this._filterServiceMessages({serviceName, messageType: MessageType.ERROR}); +- this.emit('verification-failed', {serviceName, canRetry}); ++ this.emit('verification-failed', serviceName, canRetry); + } + + get selectedMechanism() { +-- +2.55.0 + + +From a5a765f8333d0ca0042f75e993b55b6be718bd1c Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Wed, 12 Aug 2026 12:42:29 +0200 +Subject: [PATCH 11/47] gdm: Add backward-compatible property and method shims + +Several ShellUserVerifier properties and methods were dropped when +the class gained support for multiple concurrently-active AuthServices +(reauthenticating, smartcardDetected, serviceIsForeground(), +foregroundServiceDeterminesUsername(), serviceIsDefault(), +serviceIsFingerprint(), finishMessageQueue()), and AuthPrompt's +addCharacter() was renamed to startPreemptiveInput(). + +Restore reauthenticating as a real, tracked property, and add the +rest back as deprecated shims (documented with why they can no longer +be meaningfully implemented) so extensions calling them don't crash +with a TypeError. +--- + js/gdm/authPrompt.js | 8 ++++++ + js/gdm/userVerifier.js | 61 ++++++++++++++++++++++++++++++++++++++++++ + 2 files changed, 69 insertions(+) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index e874e459d..40b41177b 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -922,6 +922,14 @@ var AuthPrompt = GObject.registerClass({ + this._entry.clutter_text.insert_unichar(unichar); + } + ++ /** ++ * @deprecated Renamed to startPreemptiveInput(), kept for backward ++ * compatibility. ++ */ ++ addCharacter(unichar) { ++ this.startPreemptiveInput(unichar); ++ } ++ + _clearPreemptiveState() { + this._preemptiveInput = false; + this._preemptiveAnswer = null; +diff --git a/js/gdm/userVerifier.js b/js/gdm/userVerifier.js +index b8fb22c24..07ab56dfa 100644 +--- a/js/gdm/userVerifier.js ++++ b/js/gdm/userVerifier.js +@@ -125,6 +125,9 @@ var ShellUserVerifier = class { + this._messageQueue = []; + this._messageQueueTimeoutId = 0; + ++ // Kept for backward compatibility, see begin()/_getUserVerifierProxies(). ++ this.reauthenticating = false; ++ + this._settings = new Gio.Settings({schema_id: LOGIN_SCREEN_SCHEMA}); + this._settings.connect('changed', () => this._onSettingsChanged()); + this._updateAuthServices(); +@@ -142,9 +145,21 @@ var ShellUserVerifier = class { + return this._messageQueue ? this._messageQueue[0] : null; + } + ++ /** ++ * @deprecated Kept for backward compatibility, always returns false. ++ * Smartcard detection is now internal to AuthServicesLegacy and no ++ * longer surfaced as a top-level property; the 'smartcard-status-changed' ++ * signal has also been removed. Use the 'mechanisms-changed' signal and ++ * check a mechanism's role instead. ++ */ ++ get smartcardDetected() { ++ return false; ++ } ++ + async begin(userName, hold) { + this._cancellable?.cancel(); + this._cancellable = new Gio.Cancellable(); ++ this.reauthenticating = false; + + let resolve, reject; + const promise = new Promise((res, rej) => { +@@ -184,6 +199,51 @@ var ShellUserVerifier = class { + return this._authServices.some(s => s.needsUsername()); + } + ++ /** ++ * @deprecated Kept for backward compatibility. Replaced by needsUsername(), ++ * of which this is the logical inverse: the "foreground service" concept ++ * no longer exists now that multiple AuthServices can be active at once, ++ * each exposing its own selectable mechanisms. ++ */ ++ foregroundServiceDeterminesUsername() { ++ return !this.needsUsername(); ++ } ++ ++ /** ++ * @deprecated Kept for backward compatibility, always returns false. ++ * There is no single "foreground service" anymore: each AuthServices ++ * instance tracks its own selected mechanism (see selectedMechanism). ++ */ ++ serviceIsForeground(_serviceName) { ++ return false; ++ } ++ ++ /** ++ * @deprecated Kept for backward compatibility, always returns false. ++ * Service roles are now assigned dynamically across AuthServices ++ * instances (see _redistributeRoles()) rather than through a single ++ * fixed default service. ++ */ ++ serviceIsDefault(_serviceName) { ++ return false; ++ } ++ ++ /** ++ * @deprecated Kept for backward compatibility, always returns false. ++ * Fingerprint handling is now internal to AuthServicesLegacy; use the ++ * 'mechanisms-changed' signal and check a mechanism's role instead. ++ */ ++ serviceIsFingerprint(_serviceName) { ++ return false; ++ } ++ ++ /** ++ * @deprecated Renamed to the private _finishMessageQueue(). ++ */ ++ finishMessageQueue() { ++ this._finishMessageQueue(); ++ } ++ + setDriverService(serviceName) { + if (this._driverService === serviceName) + return; +@@ -391,6 +451,7 @@ var ShellUserVerifier = class { + try { + proxies.userVerifier = await this._client.open_reauthentication_channel( + userName, cancellable); ++ this.reauthenticating = true; + } catch (e) { + if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + throw e; +-- +2.55.0 + + +From 35df8758d507fd28a517c9962d6ad9f2fc966585 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Wed, 12 Aug 2026 13:08:48 +0200 +Subject: [PATCH 12/47] authPrompt: updateSensitivity() signature + +updateSensitivity() started requiring a {sensitive} options object +instead of accepting a plain boolean. Restore its call signature. + +This also fixes _onUserStoppedTypePreemptiveAnswer(), which was +already calling updateSensitivity(false) with a bare boolean and +silently getting undefined out of the {sensitive} destructuring. +--- + js/gdm/authPrompt.js | 6 +++++- + 1 file changed, 5 insertions(+), 1 deletion(-) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index 40b41177b..7fdcec32e 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -782,7 +782,11 @@ var AuthPrompt = GObject.registerClass({ + Util.wiggle(this._message, wiggleParameters); + } + +- updateSensitivity({sensitive}) { ++ updateSensitivity(params) { ++ // Kept for backward compatibility: this used to take a plain ++ // boolean instead of a {sensitive} options object. ++ const {sensitive} = typeof params === 'boolean' ? {sensitive: params} : params; ++ + if (sensitive && this._preemptiveAnswer) + return; + +-- +2.55.0 + + +From 1d746c07b5f66eee6e80f1e26f00cc180d9d7a2b Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Wed, 12 Aug 2026 17:08:36 +0200 +Subject: [PATCH 13/47] gdm/userVerifier: needsUsername should use every + +When every authServices needUsername then return true, otherwise return +false. +--- + js/gdm/userVerifier.js | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/js/gdm/userVerifier.js b/js/gdm/userVerifier.js +index 07ab56dfa..5d9d020fb 100644 +--- a/js/gdm/userVerifier.js ++++ b/js/gdm/userVerifier.js +@@ -196,7 +196,7 @@ var ShellUserVerifier = class { + } + + needsUsername() { +- return this._authServices.some(s => s.needsUsername()); ++ return this._authServices.every(s => s.needsUsername()); + } + + /** +-- +2.55.0 + + +From e8581a85271b38aec33674a3c50b06f6b0cc0161 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Thu, 13 Aug 2026 13:18:41 +0200 +Subject: [PATCH 14/47] gdm/authServicesLegacy: Fix credentialManagers + authentication + +There were a few guards to ensure authentication only works for +the selectedMechanism, however for credentialManagers, the selectedMechanism +isn't used. That was stopping authenticating through the credentialManager flow. +Bypass those guards only for credentialManagers. +--- + js/gdm/authServicesLegacy.js | 44 ++++++++++++++++++++++++------------ + 1 file changed, 30 insertions(+), 14 deletions(-) + +diff --git a/js/gdm/authServicesLegacy.js b/js/gdm/authServicesLegacy.js +index f257b55fc..b7592c065 100644 +--- a/js/gdm/authServicesLegacy.js ++++ b/js/gdm/authServicesLegacy.js +@@ -258,17 +258,12 @@ export class AuthServicesLegacy extends AuthServices { + } + + _handleOnSecretInfoQuery(serviceName, secretQuestion) { +- if (serviceName !== this._selectedMechanism?.serviceName) ++ // Try to auto-fill with credential manager token first ++ if (this._tryCredentialManagerAutoFill(serviceName)) + return; + +- let token = null; +- if (this._credentialManagers[serviceName]) +- token = this._credentialManagers[serviceName].token; +- +- if (token) { +- this.answerQuery(serviceName, token); ++ if (serviceName !== this._selectedMechanism?.serviceName) + return; +- } + + this.emit('ask-question', { + serviceName, +@@ -277,6 +272,20 @@ export class AuthServicesLegacy extends AuthServices { + }); + } + ++ _tryCredentialManagerAutoFill(serviceName) { ++ const credentialManager = this._credentialManagers[serviceName]; ++ if (!credentialManager) ++ return false; ++ ++ const token = credentialManager.token; ++ if (!token) ++ return false; ++ ++ this._userVerifier.call_answer_query( ++ serviceName, token, this._cancellable).catch(logErrorUnlessCancelled); ++ return true; ++ } ++ + _handleOnConversationStopped(serviceName) { + if (serviceName !== this._selectedMechanism?.serviceName && + serviceName !== FINGERPRINT_SERVICE_NAME) +@@ -332,13 +341,8 @@ export class AuthServicesLegacy extends AuthServices { + } + + _handleOnVerificationComplete(serviceName) { +- if (serviceName !== this._selectedMechanism?.serviceName) +- return; +- +- if (this._credentialManagers[serviceName]) { ++ if (this._credentialManagers[serviceName]) + this._credentialManagers[serviceName].token = null; +- this._selectedMechanism = null; +- } + } + + _handleOnChoiceListQuery(serviceName, promptMessage, list) { +@@ -357,12 +361,24 @@ export class AuthServicesLegacy extends AuthServices { + } + + _handleCanStartService(serviceName) { ++ if (this._hasAnyCredentialManagerToken()) ++ return this._hasCredentialManagerToken(serviceName); ++ + return serviceName === this._selectedMechanism?.serviceName || + (serviceName === FINGERPRINT_SERVICE_NAME && + this._enabledMechanisms.some(m => m.serviceName === serviceName) && + this._userName); + } + ++ _hasAnyCredentialManagerToken() { ++ return Object.values(this._credentialManagers).some(cm => cm.token !== null); ++ } ++ ++ _hasCredentialManagerToken(serviceName) { ++ const credentialManager = this._credentialManagers[serviceName]; ++ return credentialManager && credentialManager.token !== null; ++ } ++ + addCredentialManager(serviceName, credentialManager) { + if (this._credentialManagers[serviceName]) + return; +-- +2.55.0 + + +From 42b2e8a6bfb662f8ac1b2eb16198caf2f149a34e Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Mon, 10 Aug 2026 19:04:16 +0200 +Subject: [PATCH 15/47] gdm: Fix importing using the old gjs style + +--- + js/gdm/authList.js | 4 +- + js/gdm/authMenuButton.js | 14 +++---- + js/gdm/authPrompt.js | 4 +- + js/gdm/authServices.js | 21 +++++----- + js/gdm/authServicesLegacy.js | 26 ++++++------- + js/gdm/authServicesSSSDSwitchable.js | 18 ++++----- + js/gdm/fido2TokenManager.js | 7 ++-- + js/gdm/fingerprintManager.js | 12 +++--- + js/gdm/settings.js | 30 ++++++++------ + js/gdm/userVerifier.js | 7 ++-- + js/gdm/util.js | 58 ++++++++++++++++------------ + js/gdm/webLogin.js | 13 +++---- + js/misc/smartcardManager.js | 7 +++- + js/ui/qrCode.js | 10 ++--- + 14 files changed, 122 insertions(+), 109 deletions(-) + +diff --git a/js/gdm/authList.js b/js/gdm/authList.js +index 551442372..4550cabab 100644 +--- a/js/gdm/authList.js ++++ b/js/gdm/authList.js +@@ -19,8 +19,8 @@ + + const { Clutter, Graphene, GObject, Meta, Pango, Shell, St } = imports.gi; + +-import * as Main from '../ui/main.js'; +-import * as PopupMenu from '../ui/popupMenu.js'; ++const Main = imports.ui.main; ++const PopupMenu = imports.ui.popupMenu; + + const SCROLL_ANIMATION_TIME = 500; + +diff --git a/js/gdm/authMenuButton.js b/js/gdm/authMenuButton.js +index ca9a0b9d1..b021ce8ae 100644 +--- a/js/gdm/authMenuButton.js ++++ b/js/gdm/authMenuButton.js +@@ -42,14 +42,12 @@ * deactivates any other active item in the same section. */ @@ -435,97 +1699,374 @@ index 9b7cebf6d8..f7f458dec8 100644 -import GObject from 'gi://GObject'; -import Shell from 'gi://Shell'; -import St from 'gi://St'; -+const { Atk, Clutter, GObject, Shell, St } = imports.gi; - --import * as BoxPointer from '../ui/boxpointer.js'; +- -import * as Main from '../ui/main.js'; -import * as PopupMenu from '../ui/popupMenu.js'; -+const BoxPointer = imports.ui.boxpointer; ++/* exported AuthMenuButton, AuthMenuButtonIndicator */ ++ ++const { Atk, Clutter, GObject, Shell, St } = imports.gi; ++ +const Main = imports.ui.main; +const PopupMenu = imports.ui.popupMenu; const VISIBILITY_ANIMATION_TIME = 200; +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index 7fdcec32e..abb8b5efa 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -1,5 +1,5 @@ + // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- +-/* exported AuthPrompt */ ++/* exported AuthPrompt, ResetType, BeginRequestType */ + + const { Clutter, Gio, GLib, GObject, Meta, Pango, Shell, St } = imports.gi; + +@@ -43,7 +43,7 @@ var ResetType = { + }; + + // Kept for backward compatibility: ResetType used to be named BeginRequestType. +-export const BeginRequestType = ResetType; ++var BeginRequestType = ResetType; + + var AuthPrompt = GObject.registerClass({ + Signals: { diff --git a/js/gdm/authServices.js b/js/gdm/authServices.js -index 0a94f493a2..0cafd2831c 100644 +index e5c60e1d7..14226cfeb 100644 --- a/js/gdm/authServices.js +++ b/js/gdm/authServices.js -@@ -1,16 +1,12 @@ - // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- - --import * as FingerprintManager from '../misc/fingerprintManager.js'; +@@ -1,11 +1,9 @@ -import * as Params from '../misc/params.js'; -import {registerDestroyableType} from '../misc/signalTracker.js'; --import * as PasskeyDeviceManager from '../misc/passkeyDeviceManager.js'; --import * as SmartcardManager from '../misc/smartcardManager.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'; -import GObject from 'gi://GObject'; ++/* exported Role, RoleProperties, AuthServices */ ++ +const { Gdm, Gio, GLib, GObject } = imports.gi; + -+const FingerprintManager = imports.misc.fingerprintManager; +const Params = imports.misc.params; -+const PasskeyDeviceManager = imports.misc.passkeyDeviceManager; -+const SmartcardManager = imports.misc.smartcardManager; -+const Util = imports.gdm.util; ++const { InitError, MessageType } = imports.gdm.userVerifier; Gio._promisify(Gdm.Client.prototype, 'open_reauthentication_channel'); Gio._promisify(Gdm.Client.prototype, 'get_user_verifier'); +@@ -15,7 +13,7 @@ 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 = { ++var Role = { + PASSWORD: 'password', + SMARTCARD: 'smartcard', + FINGERPRINT: 'fingerprint', +@@ -23,7 +21,7 @@ export const Role = { + WEB_LOGIN: 'eidp', + }; + +-export const RoleProperties = { ++var RoleProperties = { + [Role.PASSWORD]: { + selectable: true, + preemptiveInput: true, +@@ -45,7 +43,7 @@ export const RoleProperties = { + }, + }; + +-export class AuthServices extends GObject.Object { ++var AuthServices = class AuthServices extends GObject.Object { + static [GObject.signals] = { + 'destroy': {}, + /** +@@ -113,7 +111,6 @@ export class AuthServices extends GObject.Object { + + static { + GObject.registerClass(this); +- registerDestroyableType(this); + } + + static SupportedRoles = []; diff --git a/js/gdm/authServicesLegacy.js b/js/gdm/authServicesLegacy.js -index c1ff1242dc..dd134db4b9 100644 +index b7592c065..fd7105553 100644 --- a/js/gdm/authServicesLegacy.js +++ b/js/gdm/authServicesLegacy.js -@@ -1,13 +1,11 @@ +@@ -1,15 +1,15 @@ -import GLib from 'gi://GLib'; -import GObject from 'gi://GObject'; - --import * as Constants from './constants.js'; --import {FingerprintReaderType} from '../misc/fingerprintManager.js'; +-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 Util from './util.js'; -import * as Vmware from './vmware.js'; --import {AuthServices} from './authServices.js'; +-import {AuthServices, Role, RoleProperties} from './authServices.js'; +-import {MessageType} from './userVerifier.js'; ++/* exported AuthServicesLegacy */ ++ +const { GLib, GObject } = imports.gi; + -+const {AuthServices} = imports.gdm.authServices; -+const Constants = imports.gdm.constants; -+const {FingerprintReaderType} = imports.misc.fingerprintManager; ++const FingerprintManager = imports.gdm.fingerprintManager; ++const { FingerprintReaderType } = imports.gdm.fingerprintManager; ++const Settings = imports.gdm.settings; ++const SmartcardManager = imports.gdm.smartcardManager; +const OVirt = imports.gdm.oVirt; -+const Util = imports.gdm.util; +const Vmware = imports.gdm.vmware; ++const { AuthServices, Role, RoleProperties } = imports.gdm.authServices; ++const { MessageType } = imports.gdm.userVerifier; const FINGERPRINT_ERROR_TIMEOUT_WAIT = 15; const FINGERPRINT_READY_TIMEOUT_MS = 500; +@@ -39,7 +39,7 @@ const Mechanisms = [ + }, + ]; + +-export class AuthServicesLegacy extends AuthServices { ++var AuthServicesLegacy = class AuthServicesLegacy extends AuthServices { + static SupportedRoles = [ + Role.PASSWORD, + Role.SMARTCARD, diff --git a/js/gdm/authServicesSSSDSwitchable.js b/js/gdm/authServicesSSSDSwitchable.js -index de03dec1d7..4f74a2040d 100644 +index 3a81b99c2..7f318bd55 100644 --- a/js/gdm/authServicesSSSDSwitchable.js +++ b/js/gdm/authServicesSSSDSwitchable.js -@@ -1,10 +1,8 @@ +@@ -1,12 +1,12 @@ -import GLib from 'gi://GLib'; -import GObject from 'gi://GObject'; -+const { GLib, GObject } = imports.gi; ++/* exported AuthServicesSSSDSwitchable */ --import * as Constants from './constants.js'; +-import * as Fido2TokenManager from './fido2TokenManager.js'; -import {logErrorUnlessCancelled} from '../misc/errorUtils.js'; --import * as Util from './util.js'; --import {AuthServices} from './authServices.js'; -+const {AuthServices} = imports.gdm.authServices; -+const Constants = imports.gdm.constants; -+const Util = imports.gdm.util; +-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 { GLib, GObject } = imports.gi; ++ ++const Fido2TokenManager = imports.gdm.fido2TokenManager; ++const SmartcardManager = imports.gdm.smartcardManager; ++const { AuthServices, Role } = imports.gdm.authServices; ++const { MessageType } = imports.gdm.userVerifier; ++const { SWITCHABLE_AUTHENTICATION_KEY } = imports.gdm.settings; - const MechanismsStatus = { - WAITING: 0, + const SWITCHABLE_AUTH_SERVICE_NAME = 'gdm-switchable-auth'; + +@@ -30,7 +30,7 @@ const PromptStatus = { + WEB_LOGIN_DIALOG_WAITING: 10, + }; + +-export class AuthServicesSSSDSwitchable extends AuthServices { ++var AuthServicesSSSDSwitchable = class AuthServicesSSSDSwitchable extends AuthServices { + static SupportedRoles = [ + Role.PASSWORD, + Role.SMARTCARD, +diff --git a/js/gdm/fido2TokenManager.js b/js/gdm/fido2TokenManager.js +index 82ed2e094..3c05061b1 100644 +--- a/js/gdm/fido2TokenManager.js ++++ b/js/gdm/fido2TokenManager.js +@@ -1,12 +1,13 @@ +-import GObject from 'gi://GObject'; +-import GUdev from 'gi://GUdev'; ++/* exported getFido2TokenManager */ ++ ++const { GObject, GUdev } = imports.gi; + + let _fido2TokenManager = null; + + /** + * @returns {Fido2TokenManager} + */ +-export function getFido2TokenManager() { ++function getFido2TokenManager() { + if (_fido2TokenManager == null) + _fido2TokenManager = new Fido2TokenManager(); + +diff --git a/js/gdm/fingerprintManager.js b/js/gdm/fingerprintManager.js +index 2577facbb..b5f391f6d 100644 +--- a/js/gdm/fingerprintManager.js ++++ b/js/gdm/fingerprintManager.js +@@ -1,15 +1,15 @@ +-import Gio from 'gi://Gio'; +-import GLib from 'gi://GLib'; +-import GObject from 'gi://GObject'; ++const { Gio, GLib, GObject } = imports.gi; + +-import {loadInterfaceXML} from '../misc/fileUtils.js'; ++/* exported FingerprintReaderType, getFingerprintManager */ ++ ++const { loadInterfaceXML } = imports.misc.fileUtils; + + const FprintManagerInfo = Gio.DBusInterfaceInfo.new_for_xml( + loadInterfaceXML('net.reactivated.Fprint.Manager')); + const FprintDeviceInfo = Gio.DBusInterfaceInfo.new_for_xml( + loadInterfaceXML('net.reactivated.Fprint.Device')); + +-export const FingerprintReaderType = { ++var FingerprintReaderType = { + NONE: 0, + PRESS: 1, + SWIPE: 2, +@@ -20,7 +20,7 @@ let _fingerprintManager = null; + /** + * @returns {FingerprintManager} + */ +-export function getFingerprintManager() { ++function getFingerprintManager() { + if (_fingerprintManager == null) + _fingerprintManager = new FingerprintManager(); + +diff --git a/js/gdm/settings.js b/js/gdm/settings.js +index bb44b0adb..2bbc0b535 100644 +--- a/js/gdm/settings.js ++++ b/js/gdm/settings.js +@@ -1,14 +1,20 @@ +-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'; ++/* exported LOGIN_SCREEN_SCHEMA, BANNER_MESSAGE_KEY, BANNER_MESSAGE_SOURCE_KEY, ++ BANNER_MESSAGE_TEXT_KEY, BANNER_MESSAGE_PATH_KEY, ALLOWED_FAILURES_KEY, ++ LOGO_KEY, DISABLE_USER_LIST_KEY, PASSWORD_AUTHENTICATION_KEY, ++ FINGERPRINT_AUTHENTICATION_KEY, SMARTCARD_AUTHENTICATION_KEY, ++ SWITCHABLE_AUTHENTICATION_KEY */ + +-export const LOGO_KEY = 'logo'; +-export const DISABLE_USER_LIST_KEY = 'disable-user-list'; ++var LOGIN_SCREEN_SCHEMA = 'org.gnome.login-screen'; ++var BANNER_MESSAGE_KEY = 'banner-message-enable'; ++var BANNER_MESSAGE_SOURCE_KEY = 'banner-message-source'; ++var BANNER_MESSAGE_TEXT_KEY = 'banner-message-text'; ++var BANNER_MESSAGE_PATH_KEY = 'banner-message-path'; ++var ALLOWED_FAILURES_KEY = 'allowed-failures'; + +-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'; ++var LOGO_KEY = 'logo'; ++var DISABLE_USER_LIST_KEY = 'disable-user-list'; ++ ++var PASSWORD_AUTHENTICATION_KEY = 'enable-password-authentication'; ++var FINGERPRINT_AUTHENTICATION_KEY = 'enable-fingerprint-authentication'; ++var SMARTCARD_AUTHENTICATION_KEY = 'enable-smartcard-authentication'; ++var SWITCHABLE_AUTHENTICATION_KEY = 'enable-switchable-authentication'; +diff --git a/js/gdm/userVerifier.js b/js/gdm/userVerifier.js +index 5d9d020fb..80609e765 100644 +--- a/js/gdm/userVerifier.js ++++ b/js/gdm/userVerifier.js +@@ -1,6 +1,7 @@ + // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + /* exported BANNER_MESSAGE_KEY, BANNER_MESSAGE_TEXT_KEY, LOGO_KEY, +- DISABLE_USER_LIST_KEY, fadeInActor, fadeOutActor */ ++ DISABLE_USER_LIST_KEY, fadeInActor, fadeOutActor, ++ MessageType, InitError, ShellUserVerifier */ + + const { Clutter, Gio, GLib } = imports.gi; + const Signals = imports.signals; +@@ -34,7 +35,7 @@ var MESSAGE_TIME_MULTIPLIER = (() => { + * + * @enum {number} + */ +-export const MessageType = { ++var MessageType = { + NONE: 0, + HINT: 1, + INFO: 2, +@@ -102,7 +103,7 @@ const AuthServicesClasses = [ + * a service via beginVerification fails. It wraps the underlying error + * and provides context about which service failed. + */ +-export class InitError extends Error { ++var InitError = class InitError extends Error { + constructor(error, message, serviceName) { + super(message, {cause: error}); + this.serviceName = serviceName; +diff --git a/js/gdm/util.js b/js/gdm/util.js +index d9a7e2239..e53620047 100644 +--- a/js/gdm/util.js ++++ b/js/gdm/util.js +@@ -1,35 +1,45 @@ +-import Clutter from 'gi://Clutter'; ++// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- ++/* exported ShellUserVerifier, MessageType, LOGIN_SCREEN_SCHEMA, ++ PASSWORD_AUTHENTICATION_KEY, FINGERPRINT_AUTHENTICATION_KEY, ++ SMARTCARD_AUTHENTICATION_KEY, BANNER_MESSAGE_KEY, BANNER_MESSAGE_SOURCE_KEY, ++ BANNER_MESSAGE_TEXT_KEY, BANNER_MESSAGE_PATH_KEY, ALLOWED_FAILURES_KEY, ++ LOGO_KEY, DISABLE_USER_LIST_KEY, PASSWORD_SERVICE_NAME, ++ FINGERPRINT_SERVICE_NAME, SMARTCARD_SERVICE_NAME, cloneAndFadeOutActor */ + +-import * as Batch from './batch.js'; +-import * as Main from '../ui/main.js'; ++const { Clutter } = imports.gi; ++ ++const Batch = imports.gdm.batch; ++const Settings = imports.gdm.settings; ++const Main = imports.ui.main; ++const UserVerifier = imports.gdm.userVerifier; + + // Re-exported for backward compatibility: these symbols used to live in + // this file before being split into userVerifier.js and settings.js. +-export {MessageType, ShellUserVerifier} from './userVerifier.js'; +-export { +- LOGIN_SCREEN_SCHEMA, +- PASSWORD_AUTHENTICATION_KEY, +- FINGERPRINT_AUTHENTICATION_KEY, +- SMARTCARD_AUTHENTICATION_KEY, +- BANNER_MESSAGE_KEY, +- BANNER_MESSAGE_SOURCE_KEY, +- BANNER_MESSAGE_TEXT_KEY, +- BANNER_MESSAGE_PATH_KEY, +- ALLOWED_FAILURES_KEY, +- LOGO_KEY, +- DISABLE_USER_LIST_KEY, +-} from './settings.js'; +- +-export const PASSWORD_SERVICE_NAME = 'gdm-password'; +-export const FINGERPRINT_SERVICE_NAME = 'gdm-fingerprint'; +-export const SMARTCARD_SERVICE_NAME = 'gdm-smartcard'; +- +-export const CLONE_FADE_ANIMATION_TIME = 250; ++var ShellUserVerifier = UserVerifier.ShellUserVerifier; ++var MessageType = UserVerifier.MessageType; ++ ++var LOGIN_SCREEN_SCHEMA = Settings.LOGIN_SCREEN_SCHEMA; ++var PASSWORD_AUTHENTICATION_KEY = Settings.PASSWORD_AUTHENTICATION_KEY; ++var FINGERPRINT_AUTHENTICATION_KEY = Settings.FINGERPRINT_AUTHENTICATION_KEY; ++var SMARTCARD_AUTHENTICATION_KEY = Settings.SMARTCARD_AUTHENTICATION_KEY; ++var BANNER_MESSAGE_KEY = Settings.BANNER_MESSAGE_KEY; ++var BANNER_MESSAGE_SOURCE_KEY = Settings.BANNER_MESSAGE_SOURCE_KEY; ++var BANNER_MESSAGE_TEXT_KEY = Settings.BANNER_MESSAGE_TEXT_KEY; ++var BANNER_MESSAGE_PATH_KEY = Settings.BANNER_MESSAGE_PATH_KEY; ++var ALLOWED_FAILURES_KEY = Settings.ALLOWED_FAILURES_KEY; ++var LOGO_KEY = Settings.LOGO_KEY; ++var DISABLE_USER_LIST_KEY = Settings.DISABLE_USER_LIST_KEY; ++ ++var PASSWORD_SERVICE_NAME = 'gdm-password'; ++var FINGERPRINT_SERVICE_NAME = 'gdm-fingerprint'; ++var SMARTCARD_SERVICE_NAME = 'gdm-smartcard'; ++ ++var CLONE_FADE_ANIMATION_TIME = 250; + + /** + * @param {Clutter.Actor} actor + */ +-export function cloneAndFadeOutActor(actor) { ++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 diff --git a/js/gdm/webLogin.js b/js/gdm/webLogin.js -index 8f2a0d8767..ef4f28b592 100644 +index 34c371b8d..dfc323540 100644 --- a/js/gdm/webLogin.js +++ b/js/gdm/webLogin.js -@@ -3,13 +3,13 @@ +@@ -1,12 +1,11 @@ // A widget showing a URL for web login - /* exported WebLoginPrompt */ ++/* exported WebLoginPrompt, WebLoginDialog */ -import Clutter from 'gi://Clutter'; -import GObject from 'gi://GObject'; @@ -534,51 +2075,30 @@ index 8f2a0d8767..ef4f28b592 100644 -import {Spinner} from '../ui/animation.js'; -import * as Params from '../misc/params.js'; -import {QrCode} from '../ui/qrCode.js'; -+const { Clutter, Gio, GnomeQR, GObject, Pango, St } = imports.gi; ++const { Clutter, GObject, Pango, St } = imports.gi; + -+const Params = imports.misc.params; +const { Spinner } = imports.ui.animation; ++const Params = imports.misc.params; +const { QrCode } = imports.ui.qrCode; -+ -+Gio._promisify(GnomeQR, 'generate_qr_code_async'); const QR_CODE_SIZE = 150; const WEB_LOGIN_SPINNER_SIZE = 35; -diff --git a/js/misc/fingerprintManager.js b/js/misc/fingerprintManager.js -index 6ef2d68d3a..cdeb3579d1 100644 ---- a/js/misc/fingerprintManager.js -+++ b/js/misc/fingerprintManager.js -@@ -1,13 +1,11 @@ --import Gio from 'gi://Gio'; --import GLib from 'gi://GLib'; --import GObject from 'gi://GObject'; -+const { Gio, GLib, GObject } = imports.gi; - --import {loadInterfaceXML} from './fileUtils.js'; -+const FileUtils = imports.misc.fileUtils; - - const FprintManagerInfo = Gio.DBusInterfaceInfo.new_for_xml( -- loadInterfaceXML('net.reactivated.Fprint.Manager')); -+ FileUtils.loadInterfaceXML('net.reactivated.Fprint.Manager')); - const FprintDeviceInfo = Gio.DBusInterfaceInfo.new_for_xml( -- loadInterfaceXML('net.reactivated.Fprint.Device')); -+ FileUtils.loadInterfaceXML('net.reactivated.Fprint.Device')); - - export const FingerprintReaderType = { - NONE: 0, -diff --git a/js/misc/passkeyDeviceManager.js b/js/misc/passkeyDeviceManager.js -index 20799975aa..2aa81d2ce6 100644 ---- a/js/misc/passkeyDeviceManager.js -+++ b/js/misc/passkeyDeviceManager.js -@@ -1,5 +1,4 @@ --import GObject from 'gi://GObject'; --import GUdev from 'gi://GUdev'; -+const { GObject, GUdev } = imports.gi; - - let _passkeyDeviceManager = null; - +diff --git a/js/misc/smartcardManager.js b/js/misc/smartcardManager.js +index df62ec048..be6146ec5 100644 +--- a/js/misc/smartcardManager.js ++++ b/js/misc/smartcardManager.js +@@ -1,3 +1,8 @@ ++// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- ++/* exported getSmartcardManager */ ++ ++const SmartcardManager = imports.gdm.smartcardManager; ++ + // Re-exported for backward compatibility: this module used to live here + // before being moved to gdm/smartcardManager.js. +-export {getSmartcardManager} from '../gdm/smartcardManager.js'; ++var getSmartcardManager = SmartcardManager.getSmartcardManager; diff --git a/js/ui/qrCode.js b/js/ui/qrCode.js -index caf0999f34..d1fac46847 100644 +index caf0999f3..d1fac4684 100644 --- a/js/ui/qrCode.js +++ b/js/ui/qrCode.js @@ -1,10 +1,6 @@ @@ -596,70 +2116,214 @@ index caf0999f34..d1fac46847 100644 Gio._promisify(GnomeQR, 'generate_qr_code_async'); -- -2.51.0 +2.55.0 -From 12ea582a78b26f37c9c7bdd12af35718cbbae961 Mon Sep 17 00:00:00 2001 +From abf4f1f4f4ed13c1a7ee77cb43e4424334873077 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez -Date: Fri, 26 Dec 2025 14:58:42 +0100 -Subject: [PATCH 03/30] gdm: Fix using the old gjs style for classes and +Date: Tue, 11 Aug 2026 16:42:35 +0200 +Subject: [PATCH 16/47] gdm: Fix importing elements from userVerifier + +This was making an infinite importing loop, so it wasn't possible to use +the imports. +--- + js/gdm/authServices.js | 8 ++++---- + js/gdm/authServicesLegacy.js | 10 +++++----- + js/gdm/authServicesSSSDSwitchable.js | 8 ++++---- + 3 files changed, 13 insertions(+), 13 deletions(-) + +diff --git a/js/gdm/authServices.js b/js/gdm/authServices.js +index 14226cfeb..8e29d92d9 100644 +--- a/js/gdm/authServices.js ++++ b/js/gdm/authServices.js +@@ -3,7 +3,7 @@ + const { Gdm, Gio, GLib, GObject } = imports.gi; + + const Params = imports.misc.params; +-const { InitError, MessageType } = imports.gdm.userVerifier; ++const UserVerifier = imports.gdm.userVerifier; + + Gio._promisify(Gdm.Client.prototype, 'open_reauthentication_channel'); + Gio._promisify(Gdm.Client.prototype, 'get_user_verifier'); +@@ -374,7 +374,7 @@ var AuthServices = class AuthServices extends GObject.Object { + _onConversationStopped(serviceName) { + this._activeServices.delete(serviceName); + +- this.emit('filter-messages', {serviceName, messageType: MessageType.ERROR}); ++ this.emit('filter-messages', {serviceName, messageType: UserVerifier.MessageType.ERROR}); + + this._handleOnConversationStopped(serviceName); + } +@@ -386,7 +386,7 @@ var AuthServices = class AuthServices extends GObject.Object { + this.emit('queue-message', { + serviceName, + message: errorMessage, +- messageType: MessageType.ERROR, ++ messageType: UserVerifier.MessageType.ERROR, + }); + } + +@@ -474,7 +474,7 @@ var AuthServices = class AuthServices extends GObject.Object { + 'org.gnome.DisplayManager.SessionWorker.Error.ServiceUnavailable') + this._unavailableServices.add(serviceName); + +- throw new InitError(e, ++ throw new UserVerifier.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 fd7105553..f97eda237 100644 +--- a/js/gdm/authServicesLegacy.js ++++ b/js/gdm/authServicesLegacy.js +@@ -9,7 +9,7 @@ const SmartcardManager = imports.gdm.smartcardManager; + const OVirt = imports.gdm.oVirt; + const Vmware = imports.gdm.vmware; + const { AuthServices, Role, RoleProperties } = imports.gdm.authServices; +-const { MessageType } = imports.gdm.userVerifier; ++const UserVerifier = imports.gdm.userVerifier; + + const FINGERPRINT_ERROR_TIMEOUT_WAIT = 15; + const FINGERPRINT_READY_TIMEOUT_MS = 500; +@@ -188,7 +188,7 @@ var AuthServicesLegacy = class AuthServicesLegacy extends AuthServices { + this.emit('queue-message', { + serviceName, + message: info, +- messageType: MessageType.INFO, ++ messageType: UserVerifier.MessageType.INFO, + }); + } else if (serviceName === FINGERPRINT_SERVICE_NAME && + this._enabledMechanisms.some(m => m.serviceName === serviceName)) { +@@ -204,7 +204,7 @@ var AuthServicesLegacy = 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: MessageType.HINT, ++ messageType: UserVerifier.MessageType.HINT, + }); + } + } +@@ -216,7 +216,7 @@ var AuthServicesLegacy = class AuthServicesLegacy extends AuthServices { + this.emit('queue-priority-message', { + serviceName, + message: problem, +- messageType: MessageType.ERROR, ++ messageType: UserVerifier.MessageType.ERROR, + wiggle: serviceName === FINGERPRINT_SERVICE_NAME, + }); + } +@@ -328,7 +328,7 @@ var AuthServicesLegacy = class AuthServicesLegacy extends AuthServices { + this.emit('queue-message', { + serviceName, + message: errorMessage, +- messageType: MessageType.ERROR, ++ messageType: UserVerifier.MessageType.ERROR, + wiggle: serviceName === FINGERPRINT_SERVICE_NAME, + }); + } +diff --git a/js/gdm/authServicesSSSDSwitchable.js b/js/gdm/authServicesSSSDSwitchable.js +index 7f318bd55..7d590d7ac 100644 +--- a/js/gdm/authServicesSSSDSwitchable.js ++++ b/js/gdm/authServicesSSSDSwitchable.js +@@ -5,7 +5,7 @@ const { GLib, GObject } = imports.gi; + const Fido2TokenManager = imports.gdm.fido2TokenManager; + const SmartcardManager = imports.gdm.smartcardManager; + const { AuthServices, Role } = imports.gdm.authServices; +-const { MessageType } = imports.gdm.userVerifier; ++const UserVerifier = imports.gdm.userVerifier; + const { SWITCHABLE_AUTHENTICATION_KEY } = imports.gdm.settings; + + const SWITCHABLE_AUTH_SERVICE_NAME = 'gdm-switchable-auth'; +@@ -331,7 +331,7 @@ var AuthServicesSSSDSwitchable = class AuthServicesSSSDSwitchable extends AuthSe + this.emit('queue-message', { + serviceName, + message: info, +- messageType: MessageType.INFO, ++ messageType: UserVerifier.MessageType.INFO, + }); + } + } +@@ -344,7 +344,7 @@ var AuthServicesSSSDSwitchable = class AuthServicesSSSDSwitchable extends AuthSe + this.emit('queue-priority-message', { + serviceName, + message: problem, +- messageType: MessageType.ERROR, ++ messageType: UserVerifier.MessageType.ERROR, + }); + } + } +@@ -521,7 +521,7 @@ var AuthServicesSSSDSwitchable = class AuthServicesSSSDSwitchable extends AuthSe + this.emit('queue-message', { + serviceName, + message, +- messageType: MessageType.INFO, ++ messageType: UserVerifier.MessageType.INFO, + }); + } + } +-- +2.55.0 + + +From f9260815697a978149009744aafece4988aafe7c Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Mon, 10 Aug 2026 19:12:27 +0200 +Subject: [PATCH 17/47] gdm: Fix using the old gjs style for classes and functions --- - js/gdm/authList.js | 45 +++++++++------------- - js/gdm/authMenuButton.js | 57 +++++++++++----------------- - js/gdm/authServices.js | 20 ++++------ - js/gdm/authServicesLegacy.js | 15 ++++---- - js/gdm/authServicesSSSDSwitchable.js | 15 ++++---- - js/gdm/constants.js | 22 ++++++----- - js/gdm/util.js | 9 +++-- - js/gdm/webLogin.js | 20 +++++----- - js/misc/fingerprintManager.js | 30 +++++++-------- - js/misc/passkeyDeviceManager.js | 23 +++++------ - js/ui/qrCode.js | 40 +++++++++---------- - 11 files changed, 131 insertions(+), 165 deletions(-) + js/gdm/authList.js | 49 ++++++++++------------------ + js/gdm/authMenuButton.js | 47 ++++++++++---------------- + js/gdm/authServices.js | 18 ++++------ + js/gdm/authServicesLegacy.js | 13 +++----- + js/gdm/authServicesSSSDSwitchable.js | 13 +++----- + js/gdm/fido2TokenManager.js | 18 ++++------ + js/gdm/fingerprintManager.js | 23 +++++-------- + js/gdm/webLogin.js | 31 +++++++----------- + js/ui/qrCode.js | 38 ++++++++++----------- + 9 files changed, 98 insertions(+), 152 deletions(-) diff --git a/js/gdm/authList.js b/js/gdm/authList.js -index 3f1b2817f9..6eb34f6d97 100644 +index 4550cabab..5c7e1eaac 100644 --- a/js/gdm/authList.js +++ b/js/gdm/authList.js -@@ -57,13 +57,10 @@ const ItemIconPopup = class extends PopupMenu.PopupMenu { +@@ -57,13 +57,10 @@ var ItemIconPopup = class extends PopupMenu.PopupMenu { } }; -+const ItemIcon = GObject.registerClass( - class ItemIcon extends St.Button { +-var ItemIcon = class extends St.Button { - static { - GObject.registerClass(this); - } - - constructor(iconName, iconTitle, iconSubtitle) { - super({ ++var ItemIcon = GObject.registerClass( ++class ItemIcon extends St.Button { + _init(iconName, iconTitle, iconSubtitle) { + super._init({ style_class: 'login-dialog-item-icon', + button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE, iconName, - }); -@@ -71,21 +68,17 @@ class ItemIcon extends St.Button { +@@ -72,21 +69,15 @@ var ItemIcon = class extends St.Button { this._popup = new ItemIconPopup(this, iconTitle, iconSubtitle); Main.uiGroup.add_child(this._popup.actor); } -} -+}); - +- -class AuthListItem extends St.Button { - static [GObject.signals] = { -+const AuthListItem = GObject.registerClass({ -+ Signals: { - 'activate': {}, +- 'activate': {}, - }; - - static { - GObject.registerClass(this); - } -- ++}); + - constructor(key, content) { -+ }, ++var AuthListItem = GObject.registerClass({ ++ Signals: {'activate': {}}, +}, class AuthListItem extends St.Button { + _init(key, content) { const {title, subtitle, iconName, iconTitle, iconSubtitle} = content; @@ -669,14 +2333,14 @@ index 3f1b2817f9..6eb34f6d97 100644 style_class: 'login-dialog-auth-list-item', button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE, can_focus: true, -@@ -168,20 +161,16 @@ class AuthListItem extends St.Button { +@@ -173,20 +164,16 @@ class AuthListItem extends St.Button { this.remove_style_pseudo_class('selected'); } } -} +}); --export class AuthList extends St.BoxLayout { +-var AuthList = class extends St.BoxLayout { - static [GObject.signals] = { +var AuthList = GObject.registerClass({ + Signals: { @@ -686,31 +2350,32 @@ index 3f1b2817f9..6eb34f6d97 100644 - - static { - GObject.registerClass(this); - } +- } - - constructor() { - super({ ++ }, +}, class AuthList extends St.BoxLayout { + _init() { + super._init({ orientation: Clutter.Orientation.VERTICAL, style_class: 'login-dialog-auth-list-layout', y_align: Clutter.ActorAlign.CENTER, -@@ -278,4 +267,4 @@ export class AuthList extends St.BoxLayout { +@@ -297,4 +284,4 @@ var AuthList = class extends St.BoxLayout { this._box.destroy_all_children(); this._items.clear(); } -} +}); diff --git a/js/gdm/authMenuButton.js b/js/gdm/authMenuButton.js -index f7f458dec8..e58c331b45 100644 +index b021ce8ae..abb10e679 100644 --- a/js/gdm/authMenuButton.js +++ b/js/gdm/authMenuButton.js -@@ -71,13 +71,10 @@ class AuthMenuItemSection extends PopupMenu.PopupMenuSection { +@@ -70,13 +70,10 @@ class AuthMenuItemSection extends PopupMenu.PopupMenuSection { } } -+const AuthMenuItem = GObject.registerClass( ++var AuthMenuItem = GObject.registerClass( class AuthMenuItem extends PopupMenu.PopupBaseMenuItem { - static { - GObject.registerClass(this); @@ -723,9 +2388,9 @@ index f7f458dec8..e58c331b45 100644 if (item.description && item.iconName) { const icon = new St.Icon({ -@@ -117,42 +114,37 @@ class AuthMenuItem extends PopupMenu.PopupBaseMenuItem { - - this.setOrnament(PopupMenu.Ornament.HIDDEN); +@@ -114,10 +111,10 @@ class AuthMenuItem extends PopupMenu.PopupBaseMenuItem { + this.label_actor = nameLabel; + } } -} +}); @@ -734,29 +2399,12 @@ index f7f458dec8..e58c331b45 100644 - static [GObject.properties] = { +var AuthMenuButton = GObject.registerClass({ + Properties: { - 'title': GObject.ParamSpec.string( -- 'title', null, null, -+ 'title', 'title', 'title', - GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY, - ''), - 'icon-name': GObject.ParamSpec.string( -- 'icon-name', null, null, -+ 'icon-name', 'icon-name', 'icon-name', - GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY, - ''), 'read-only': GObject.ParamSpec.boolean( -- 'read-only', null, null, -+ 'read-only', 'read-only', 'read-only', - GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY, - false), - 'section-order': GObject.ParamSpec.jsobject( -- 'section-order', null, null, -+ 'section-order', 'section-order', 'section-order', - GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY), - 'animate-visibility': GObject.ParamSpec.boolean( -- 'animate-visibility', null, null, -+ 'animate-visibility', 'animate-visibility', 'animate-visibility', + 'read-only', null, null, GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY, +@@ -129,18 +126,13 @@ export class AuthMenuButton extends St.Button { + 'animate-visibility', null, null, + GObject.ParamFlags.READWRITE, false), - }; - @@ -770,17 +2418,16 @@ index f7f458dec8..e58c331b45 100644 - GObject.registerClass(this); - } - -- constructor(params) { +- constructor(params = {}) { +- super({ + }, +}, class AuthMenuButton extends St.Button { -+ _init(params) { - params.sectionOrder ??= []; -- super({ ++ _init(params = {}) { + super._init({ + icon_name: 'cog-wheel-symbolic', + sectionOrder: [], ...params, - style_class: 'login-dialog-button login-dialog-auth-menu-button', - reactive: true, -@@ -452,16 +444,13 @@ export class AuthMenuButton extends St.Button { +@@ -423,15 +415,12 @@ export class AuthMenuButton extends St.Button { closeMenu() { this._menu.close(); } @@ -793,51 +2440,43 @@ index f7f458dec8..e58c331b45 100644 - } - - constructor(params = {}) { +- super({ +var AuthMenuButtonIndicator = GObject.registerClass( +class AuthMenuButtonIndicator extends AuthMenuButton { + _init(params = {}) { - params.readOnly ??= true; -- super(params); -+ super._init(params); - - this.add_style_class_name('login-dialog-auth-menu-button-indicator'); - } -@@ -526,4 +515,4 @@ export class AuthMenuButtonIndicator extends AuthMenuButton { - // Override to force visibility even when there's only one item - _updateVisibility() { ++ super._init({ + readOnly: true, + ...params, + }); +@@ -509,4 +498,4 @@ export class AuthMenuButtonIndicator extends AuthMenuButton { + _canBeVisible() { + return this._items.size > 0; } -} +}); diff --git a/js/gdm/authServices.js b/js/gdm/authServices.js -index 0cafd2831c..ce4f0729b3 100644 +index 8e29d92d9..a6aabf8f2 100644 --- a/js/gdm/authServices.js +++ b/js/gdm/authServices.js -@@ -1,4 +1,5 @@ - // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- -+/* exported AuthServices */ +@@ -43,8 +43,8 @@ var RoleProperties = { + }, + }; - const { Gdm, Gio, GLib, GObject } = imports.gi; - -@@ -16,8 +17,8 @@ Gio._promisify(Gdm.UserVerifierProxy.prototype, 'call_answer_query'); - Gio._promisify(Gdm.UserVerifierChoiceListProxy.prototype, 'call_select_choice'); - Gio._promisify(Gdm.UserVerifierCustomJSONProxy.prototype, 'call_reply'); - --export class AuthServices extends GObject.Object { +-var AuthServices = class AuthServices extends GObject.Object { - static [GObject.signals] = { +var AuthServices = GObject.registerClass({ + Signals: { 'destroy': {}, - 'queue-message': { - param_types: [GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_UINT], -@@ -51,13 +52,8 @@ export class AuthServices extends GObject.Object { - GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_JSOBJECT, - ], - }, + /** + * @param {string} serviceName +@@ -107,12 +107,8 @@ var AuthServices = class AuthServices extends GObject.Object { + * @param {object[]} buttons + */ + 'web-login': {param_types: [GObject.TYPE_JSOBJECT]}, - }; - - static { - GObject.registerClass(this); -- registerDestroyableType(this); - } - + }, @@ -845,8 +2484,8 @@ index 0cafd2831c..ce4f0729b3 100644 static SupportedRoles = []; static RoleToService = {}; -@@ -65,8 +61,8 @@ export class AuthServices extends GObject.Object { - return roles.some(r => this.SupportedRoles.includes(r)); +@@ -124,8 +120,8 @@ var AuthServices = class AuthServices extends GObject.Object { + return Object.values(this.RoleToService).includes(serviceName); } - constructor(params) { @@ -855,273 +2494,133 @@ index 0cafd2831c..ce4f0729b3 100644 + super._init(); params = Params.parse(params, { client: null, - enabledRoles: [], -@@ -502,4 +498,4 @@ export class AuthServices extends GObject.Object { - throw new GObject.NotImplementedError( - `_handleCanStartService in ${this.constructor.name}`); - } + allowedFailures: 3, +@@ -548,4 +544,4 @@ var AuthServices = class AuthServices extends GObject.Object { + addCredentialManager(_serviceName, _credentialManager) {} + + removeCredentialManager(_serviceName) {} -} +}); diff --git a/js/gdm/authServicesLegacy.js b/js/gdm/authServicesLegacy.js -index dd134db4b9..8922546474 100644 +index f97eda237..6a1509acd 100644 --- a/js/gdm/authServicesLegacy.js +++ b/js/gdm/authServicesLegacy.js -@@ -1,3 +1,5 @@ -+/* exported AuthServicesLegacy */ -+ - const { GLib, GObject } = imports.gi; - - const {AuthServices} = imports.gdm.authServices; -@@ -28,7 +30,8 @@ const Mechanisms = [ +@@ -39,7 +39,8 @@ const Mechanisms = [ }, ]; --export class AuthServicesLegacy extends AuthServices { +-var AuthServicesLegacy = class AuthServicesLegacy extends AuthServices { +var AuthServicesLegacy = GObject.registerClass({ +}, class AuthServicesLegacy extends AuthServices { static SupportedRoles = [ - Constants.PASSWORD_ROLE_NAME, - Constants.SMARTCARD_ROLE_NAME, -@@ -41,12 +44,8 @@ export class AuthServicesLegacy extends AuthServices { - [Constants.FINGERPRINT_ROLE_NAME]: Constants.FINGERPRINT_SERVICE_NAME, + Role.PASSWORD, + Role.SMARTCARD, +@@ -52,18 +53,14 @@ var AuthServicesLegacy = class AuthServicesLegacy extends AuthServices { + [Role.FINGERPRINT]: FINGERPRINT_SERVICE_NAME, }; - static { - GObject.registerClass(this); - } - + static isEnabled(settings) { + return settings.get_boolean(Settings.PASSWORD_AUTHENTICATION_KEY) || + settings.get_boolean(Settings.FINGERPRINT_AUTHENTICATION_KEY) || + settings.get_boolean(Settings.SMARTCARD_AUTHENTICATION_KEY); + } + - constructor(params) { - super(params); + _init(params) { + super._init(params); - this._updateEnabledMechanisms(); - -@@ -378,4 +377,4 @@ export class AuthServicesLegacy extends AuthServices { + this._connectSmartcardManager(); + this._connectFingerprintManager(); +@@ -446,4 +443,4 @@ var AuthServicesLegacy = class AuthServicesLegacy extends AuthServices { }; this.emit('reset', {softReset: true}); } -} +}); diff --git a/js/gdm/authServicesSSSDSwitchable.js b/js/gdm/authServicesSSSDSwitchable.js -index 4f74a2040d..4b4761d105 100644 +index 7d590d7ac..7f5d13171 100644 --- a/js/gdm/authServicesSSSDSwitchable.js +++ b/js/gdm/authServicesSSSDSwitchable.js -@@ -1,3 +1,5 @@ -+/* exported AuthServicesSwitchable */ -+ - const { GLib, GObject } = imports.gi; - - const {AuthServices} = imports.gdm.authServices; -@@ -10,7 +12,8 @@ const MechanismsStatus = { - FOUND: 2, +@@ -30,7 +30,8 @@ const PromptStatus = { + WEB_LOGIN_DIALOG_WAITING: 10, }; --export class AuthServicesSSSDSwitchable extends AuthServices { +-var AuthServicesSSSDSwitchable = class AuthServicesSSSDSwitchable extends AuthServices { +var AuthServicesSSSDSwitchable = GObject.registerClass({ +}, class AuthServicesSSSDSwitchable extends AuthServices { static SupportedRoles = [ - Constants.PASSWORD_ROLE_NAME, - Constants.SMARTCARD_ROLE_NAME, -@@ -25,12 +28,8 @@ export class AuthServicesSSSDSwitchable extends AuthServices { - [Constants.WEB_LOGIN_ROLE_NAME]: Constants.SWITCHABLE_AUTH_SERVICE_NAME, + Role.PASSWORD, + Role.SMARTCARD, +@@ -45,16 +46,12 @@ var AuthServicesSSSDSwitchable = class AuthServicesSSSDSwitchable extends AuthSe + [Role.WEB_LOGIN]: SWITCHABLE_AUTH_SERVICE_NAME, }; - static { - GObject.registerClass(this); - } - + static isEnabled(settings) { + return settings.get_boolean(SWITCHABLE_AUTHENTICATION_KEY); + } + - constructor(params) { - super(params); + _init(params) { + super._init(params); - this._mechanismsStatus = MechanismsStatus.WAITING; - } -@@ -440,4 +439,4 @@ export class AuthServicesSSSDSwitchable extends AuthServices { - GLib.source_remove(this._webLoginTimeoutId); - this._webLoginTimeoutId = 0; + this._connectSmartcardManager(); + this._connectFido2TokenManager(); +@@ -622,4 +619,4 @@ var AuthServicesSSSDSwitchable = class AuthServicesSSSDSwitchable extends AuthSe + + this.emit('reset', {softReset: true, reuseEntryText: true}); } -} +}); -diff --git a/js/gdm/constants.js b/js/gdm/constants.js -index 7c48599e64..cfbbe32426 100644 ---- a/js/gdm/constants.js -+++ b/js/gdm/constants.js -@@ -1,12 +1,16 @@ - // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- -+/* exported PASSWORD_ROLE_NAME, SMARTCARD_ROLE_NAME, FINGERPRINT_ROLE_NAME, -+ PASSKEY_ROLE_NAME, WEB_LOGIN_ROLE_NAME, PASSWORD_SERVICE_NAME, -+ SMARTCARD_SERVICE_NAME, FINGERPRINT_SERVICE_NAME, -+ SWITCHABLE_AUTH_SERVICE_NAME */ +diff --git a/js/gdm/fido2TokenManager.js b/js/gdm/fido2TokenManager.js +index 3c05061b1..5aa2653e0 100644 +--- a/js/gdm/fido2TokenManager.js ++++ b/js/gdm/fido2TokenManager.js +@@ -14,18 +14,14 @@ function getFido2TokenManager() { + return _fido2TokenManager; + } --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'; -+var PASSWORD_ROLE_NAME = 'password'; -+var SMARTCARD_ROLE_NAME = 'smartcard'; -+var FINGERPRINT_ROLE_NAME = 'fingerprint'; -+var PASSKEY_ROLE_NAME = 'passkey'; -+var WEB_LOGIN_ROLE_NAME = 'eidp'; +-class Fido2TokenManager extends GObject.Object { +- static [GObject.signals] = { ++var Fido2TokenManager = GObject.registerClass({ ++ Signals: { + 'fido2-token-inserted': {param_types: [GObject.TYPE_JSOBJECT]}, + 'fido2-token-removed': {param_types: [GObject.TYPE_JSOBJECT]}, +- }; +- +- static { +- GObject.registerClass(this); +- } +- +- constructor() { +- super(); ++ }, ++}, class Fido2TokenManager extends GObject.Object { ++ _init() { ++ super._init(); --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'; -+var PASSWORD_SERVICE_NAME = 'gdm-password'; -+var SMARTCARD_SERVICE_NAME = 'gdm-smartcard'; -+var FINGERPRINT_SERVICE_NAME = 'gdm-fingerprint'; -+var SWITCHABLE_AUTH_SERVICE_NAME = 'gdm-switchable-auth'; -diff --git a/js/gdm/util.js b/js/gdm/util.js -index 743fce11b7..f1709edea3 100644 ---- a/js/gdm/util.js -+++ b/js/gdm/util.js -@@ -1,6 +1,7 @@ - // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- - /* exported BANNER_MESSAGE_KEY, BANNER_MESSAGE_TEXT_KEY, LOGO_KEY, -- DISABLE_USER_LIST_KEY, fadeInActor, fadeOutActor, cloneAndFadeOutActor */ -+ DISABLE_USER_LIST_KEY, fadeInActor, fadeOutActor, cloneAndFadeOutActor, -+ InitError, isSelectable, getIconName */ - - const { Clutter, Gio, GLib } = imports.gi; - const Signals = imports.signals; -@@ -125,7 +126,7 @@ function cloneAndFadeOutActor(actor) { - * a service via beginVerification fails. It wraps the underlying error - * and provides context about which service failed. - */ --export class InitError extends Error { -+var InitError = class extends Error { - constructor(error, message, serviceName) { - super(message, {cause: error}); - this.serviceName = serviceName; -@@ -136,7 +137,7 @@ export class InitError extends Error { - * @param {object} mechanism - * @returns {boolean} - */ --export function isSelectable(mechanism) { -+function isSelectable(mechanism) { - switch (mechanism.role) { - case Constants.PASSWORD_ROLE_NAME: - case Constants.SMARTCARD_ROLE_NAME: -@@ -154,7 +155,7 @@ export function isSelectable(mechanism) { - * @param {object} mechanism - * @returns {string} - */ --export function getNonSelectableIconName(mechanism) { -+function getNonSelectableIconName(mechanism) { - // This is only used for non selectable mechanisms. - // Currently only fingerprint is non selectable - if (isSelectable(mechanism)) -diff --git a/js/gdm/webLogin.js b/js/gdm/webLogin.js -index ef4f28b592..9c858f7269 100644 ---- a/js/gdm/webLogin.js -+++ b/js/gdm/webLogin.js -@@ -1,7 +1,7 @@ - // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- - // - // A widget showing a URL for web login --/* exported WebLoginPrompt */ -+/* exported QrCode, WebLoginPrompt, WebLoginDialog, WebLoginIntro */ - - const { Clutter, Gio, GnomeQR, GObject, Pango, St } = imports.gi; - -@@ -15,9 +15,9 @@ const QR_CODE_SIZE = 150; - const WEB_LOGIN_SPINNER_SIZE = 35; - const URL_LABEL_LONG_THRESHOLD = 45; - --export const WebLoginPrompt = GObject.registerClass( -+var WebLoginPrompt = GObject.registerClass( - class WebLoginPrompt extends St.BoxLayout { -- constructor(params) { -+ _init(params) { - const {qrSize: qrCodeSize, message, url, code} = Params.parse(params, { - qrSize: QR_CODE_SIZE, - message: null, -@@ -25,7 +25,7 @@ class WebLoginPrompt extends St.BoxLayout { - code: null, - }); - -- super({ -+ super._init({ - styleClass: 'web-login-prompt', - vertical: true, - y_align: Clutter.ActorAlign.CENTER, -@@ -124,13 +124,13 @@ class WebLoginPrompt extends St.BoxLayout { + this._insertedTokens = new Map(); + this._udevClient = new GUdev.Client({subsystems: ['hidraw']}); +@@ -67,4 +63,4 @@ class Fido2TokenManager extends GObject.Object { + this._insertedTokens.delete(sysfsPath); + this.emit('fido2-token-removed', device); } - }); - --export const WebLoginDialog = GObject.registerClass({ -+var WebLoginDialog = GObject.registerClass({ - Signals: { - 'cancel': {}, - 'loading': {}, - }, - }, class WebLoginDialog extends St.Widget { -- constructor(params) { -+ _init(params) { - const {message, url, code, buttons} = Params.parse(params, { - message: null, - url: null, -@@ -138,7 +138,7 @@ export const WebLoginDialog = GObject.registerClass({ - buttons: [], - }); - -- super({ -+ super._init({ - layout_manager: new Clutter.BinLayout(), - x_expand: true, - y_expand: true, -@@ -279,9 +279,9 @@ export const WebLoginDialog = GObject.registerClass({ - } - }); - --export var WebLoginIntro = GObject.registerClass( -+var WebLoginIntro = GObject.registerClass( - class WebLoginIntro extends St.Button { -- constructor(params) { -+ _init(params) { - const {message} = Params.parse(params, { - message: null, - }); -@@ -291,7 +291,7 @@ class WebLoginIntro extends St.Button { - style_class: 'web-login-button-label', - }); - -- super({ -+ super._init({ - style_class: 'web-login-intro-button', - accessible_name: message, - button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE, -diff --git a/js/misc/fingerprintManager.js b/js/misc/fingerprintManager.js -index cdeb3579d1..151e787e0b 100644 ---- a/js/misc/fingerprintManager.js -+++ b/js/misc/fingerprintManager.js -@@ -1,4 +1,5 @@ - const { Gio, GLib, GObject } = imports.gi; -+/* exported FingerprintReaderType, FingerprintManager */ - - const FileUtils = imports.misc.fileUtils; - -@@ -7,7 +8,7 @@ const FprintManagerInfo = Gio.DBusInterfaceInfo.new_for_xml( - const FprintDeviceInfo = Gio.DBusInterfaceInfo.new_for_xml( - FileUtils.loadInterfaceXML('net.reactivated.Fprint.Device')); - --export const FingerprintReaderType = { -+var FingerprintReaderType = { - NONE: 0, - PRESS: 1, - SWIPE: 2, -@@ -18,32 +19,27 @@ let _fingerprintManager = null; - /** - * @returns {FingerprintManager} - */ --export function getFingerprintManager() { -+function getFingerprintManager() { - if (_fingerprintManager == null) - _fingerprintManager = new FingerprintManager(); - +-} ++}); +diff --git a/js/gdm/fingerprintManager.js b/js/gdm/fingerprintManager.js +index b5f391f6d..d429001c5 100644 +--- a/js/gdm/fingerprintManager.js ++++ b/js/gdm/fingerprintManager.js +@@ -27,25 +27,20 @@ function getFingerprintManager() { return _fingerprintManager; } @@ -1130,8 +2629,7 @@ index cdeb3579d1..151e787e0b 100644 +var FingerprintManager = GObject.registerClass({ + Properties: { 'reader-type': GObject.ParamSpec.uint( -- 'reader-type', null, null, -+ 'reader-type', 'reader-type', 'reader-type', + 'reader-type', null, null, GObject.ParamFlags.READWRITE, FingerprintReaderType.NONE, FingerprintReaderType.SWIPE, FingerprintReaderType.NONE), @@ -1156,64 +2654,84 @@ index cdeb3579d1..151e787e0b 100644 this._fingerprintManagerProxy = new Gio.DBusProxy({ g_connection: Gio.DBus.system, -@@ -133,4 +129,4 @@ class FingerprintManager extends GObject.Object { +@@ -135,4 +130,4 @@ class FingerprintManager extends GObject.Object { logError(e, 'Failed to interact with fprintd service'); } -} +}); -diff --git a/js/misc/passkeyDeviceManager.js b/js/misc/passkeyDeviceManager.js -index 2aa81d2ce6..747ee93fc8 100644 ---- a/js/misc/passkeyDeviceManager.js -+++ b/js/misc/passkeyDeviceManager.js -@@ -1,30 +1,27 @@ - const { GObject, GUdev } = imports.gi; -+/* exported getPasskeyDeviceManager */ -+ +diff --git a/js/gdm/webLogin.js b/js/gdm/webLogin.js +index dfc323540..e3fb95d59 100644 +--- a/js/gdm/webLogin.js ++++ b/js/gdm/webLogin.js +@@ -11,12 +11,9 @@ const QR_CODE_SIZE = 150; + const WEB_LOGIN_SPINNER_SIZE = 35; + const URL_LABEL_LONG_THRESHOLD = 45; - let _passkeyDeviceManager = null; +-export class WebLoginPrompt extends St.BoxLayout { +- static { +- GObject.registerClass(this); +- } +- +- constructor(params) { ++var WebLoginPrompt = GObject.registerClass( ++class WebLoginPrompt extends St.BoxLayout { ++ _init(params) { + const {qrSize: qrCodeSize, message, url, code} = Params.parse(params, { + qrSize: QR_CODE_SIZE, + message: null, +@@ -24,7 +21,7 @@ export class WebLoginPrompt extends St.BoxLayout { + code: null, + }); - /** - * @returns {PasskeyDeviceManager} - */ --export function getPasskeyDeviceManager() { -+function getPasskeyDeviceManager() { - if (_passkeyDeviceManager == null) - _passkeyDeviceManager = new PasskeyDeviceManager(); +- super({ ++ super._init({ + styleClass: 'web-login-prompt', + vertical: true, + y_align: Clutter.ActorAlign.CENTER, +@@ -121,19 +118,15 @@ export class WebLoginPrompt extends St.BoxLayout { - return _passkeyDeviceManager; - } + return url; + } +-}; ++}); --class PasskeyDeviceManager extends GObject.Object { +-export class WebLoginDialog extends St.Widget { - static [GObject.signals] = { -+var PasskeyDeviceManager = GObject.registerClass({ ++var WebLoginDialog = GObject.registerClass({ + Signals: { - 'passkey-inserted': {param_types: [GObject.TYPE_JSOBJECT]}, - 'passkey-removed': {param_types: [GObject.TYPE_JSOBJECT]}, + 'cancel': {}, + 'loading': {}, - }; - - static { - GObject.registerClass(this); - } - -- constructor() { -- super(); -- +- constructor(params) { + }, -+}, class PasskeyDeviceManager extends GObject.Object { -+ _init() { -+ super._init(); - this._insertedPasskeys = new Map(); - this._udevClient = new GUdev.Client({subsystems: ['hidraw']}); ++}, class WebLoginDialog extends St.Widget { ++ _init(params) { + const {message, url, code, buttons} = Params.parse(params, { + message: null, + url: null, +@@ -141,7 +134,7 @@ export class WebLoginDialog extends St.Widget { + buttons: [], + }); -@@ -65,4 +62,4 @@ class PasskeyDeviceManager extends GObject.Object { - this._insertedPasskeys.delete(sysfsPath); - this.emit('passkey-removed', device); +- super({ ++ super._init({ + layout_manager: new Clutter.BinLayout(), + x_expand: true, + y_expand: true, +@@ -281,4 +274,4 @@ export class WebLoginDialog extends St.Widget { + this.isLoading = false; + this.emit('loading'); } -} +}); diff --git a/js/ui/qrCode.js b/js/ui/qrCode.js -index d1fac46847..5920e45e5a 100644 +index d1fac4684..18c9b9977 100644 --- a/js/ui/qrCode.js +++ b/js/ui/qrCode.js @@ -7,43 +7,39 @@ Gio._promisify(GnomeQR, 'generate_qr_code_async'); @@ -1225,8 +2743,7 @@ index d1fac46847..5920e45e5a 100644 +var QrCode = GObject.registerClass({ + Properties: { 'url': GObject.ParamSpec.string( -- 'url', null, null, -+ 'url', 'url', 'url', + 'url', null, null, GObject.ParamFlags.READWRITE, null), - }; @@ -1300,28 +2817,46 @@ index d1fac46847..5920e45e5a 100644 -}; +}); -- -2.51.0 +2.55.0 -From 8d635a7b5f8da824025ae9e206cd2cdf92bc83ff Mon Sep 17 00:00:00 2001 +From f7cc5e3197acff1e884b3f2f879d9c66a1a92429 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez -Date: Fri, 26 Dec 2025 15:01:19 +0100 -Subject: [PATCH 04/30] gdm: Fix using logErrorUnless cancelled +Date: Mon, 10 Aug 2026 19:15:01 +0200 +Subject: [PATCH 18/47] gdm: Fix using logErrorUnlessCancelled This function doesn't exist in this version. --- + js/gdm/authPrompt.js | 6 ++++-- js/gdm/authServices.js | 3 ++- js/gdm/authServicesLegacy.js | 12 +++++++++--- js/gdm/authServicesSSSDSwitchable.js | 12 +++++++++--- - js/gdm/util.js | 5 ++++- + js/gdm/userVerifier.js | 9 ++++++--- js/ui/qrCode.js | 13 ++++++++++--- - 5 files changed, 34 insertions(+), 11 deletions(-) + 6 files changed, 40 insertions(+), 15 deletions(-) +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index abb8b5efa..4d239dde1 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -946,8 +946,10 @@ var AuthPrompt = GObject.registerClass({ + if (!this._preemptiveInput) + this.updateSensitivity({sensitive: false}); + +- this._userVerifier.begin(params.userName, params.hold).catch( +- logErrorUnlessCancelled); ++ this._userVerifier.begin(params.userName, params.hold).catch(e => { ++ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) ++ logError(e); ++ }); + this.verificationStatus = AuthPromptStatus.VERIFYING; + } + diff --git a/js/gdm/authServices.js b/js/gdm/authServices.js -index ce4f0729b3..fea7ba0634 100644 +index a6aabf8f2..006209d39 100644 --- a/js/gdm/authServices.js +++ b/js/gdm/authServices.js -@@ -373,7 +373,8 @@ var AuthServices = GObject.registerClass({ +@@ -418,7 +418,8 @@ var AuthServices = GObject.registerClass({ await this._waitPendingMessages(); this.emit('reset', {softReset: !doneTrying}); } catch (e) { @@ -1332,7 +2867,7 @@ index ce4f0729b3..fea7ba0634 100644 } diff --git a/js/gdm/authServicesLegacy.js b/js/gdm/authServicesLegacy.js -index 8922546474..0fd0e10d81 100644 +index 6a1509acd..1b336e9d1 100644 --- a/js/gdm/authServicesLegacy.js +++ b/js/gdm/authServicesLegacy.js @@ -1,6 +1,6 @@ @@ -1341,9 +2876,9 @@ index 8922546474..0fd0e10d81 100644 -const { GLib, GObject } = imports.gi; +const { Gio, GLib, GObject } = imports.gi; - const {AuthServices} = imports.gdm.authServices; - const Constants = imports.gdm.constants; -@@ -61,7 +61,10 @@ var AuthServicesLegacy = GObject.registerClass({ + const FingerprintManager = imports.gdm.fingerprintManager; + const { FingerprintReaderType } = imports.gdm.fingerprintManager; +@@ -79,7 +79,10 @@ var AuthServicesLegacy = GObject.registerClass({ return; this._userVerifierChoiceList.call_select_choice( @@ -1355,7 +2890,7 @@ index 8922546474..0fd0e10d81 100644 } _handleAnswerQuery(serviceName, answer) { -@@ -72,7 +75,10 @@ var AuthServicesLegacy = GObject.registerClass({ +@@ -90,7 +93,10 @@ var AuthServicesLegacy = GObject.registerClass({ this._smartcardInProgress = true; this._userVerifier.call_answer_query( @@ -1368,18 +2903,18 @@ index 8922546474..0fd0e10d81 100644 _handleBeginVerification() { diff --git a/js/gdm/authServicesSSSDSwitchable.js b/js/gdm/authServicesSSSDSwitchable.js -index 4b4761d105..601f4cd295 100644 +index 7f5d13171..9625b5e82 100644 --- a/js/gdm/authServicesSSSDSwitchable.js +++ b/js/gdm/authServicesSSSDSwitchable.js @@ -1,6 +1,6 @@ - /* exported AuthServicesSwitchable */ + /* exported AuthServicesSSSDSwitchable */ -const { GLib, GObject } = imports.gi; +const { Gio, GLib, GObject } = imports.gi; - const {AuthServices} = imports.gdm.authServices; - const Constants = imports.gdm.constants; -@@ -54,7 +54,10 @@ var AuthServicesSSSDSwitchable = GObject.registerClass({ + const Fido2TokenManager = imports.gdm.fido2TokenManager; + const SmartcardManager = imports.gdm.smartcardManager; +@@ -96,7 +96,10 @@ var AuthServicesSSSDSwitchable = GObject.registerClass({ this._resettingPassword) { this._userVerifier.call_answer_query(serviceName, answer, @@ -1391,7 +2926,7 @@ index 4b4761d105..601f4cd295 100644 return; } -@@ -316,7 +319,10 @@ var AuthServicesSSSDSwitchable = GObject.registerClass({ +@@ -429,7 +432,10 @@ var AuthServicesSSSDSwitchable = GObject.registerClass({ const {serviceName} = this._selectedMechanism; this._userVerifierCustomJSON.call_reply( @@ -1403,11 +2938,22 @@ index 4b4761d105..601f4cd295 100644 } _eventExpected() { -diff --git a/js/gdm/util.js b/js/gdm/util.js -index f1709edea3..17892c75e8 100644 ---- a/js/gdm/util.js -+++ b/js/gdm/util.js -@@ -282,7 +282,10 @@ var ShellUserVerifier = class { +diff --git a/js/gdm/userVerifier.js b/js/gdm/userVerifier.js +index 80609e765..8a29d7a13 100644 +--- a/js/gdm/userVerifier.js ++++ b/js/gdm/userVerifier.js +@@ -179,8 +179,8 @@ var ShellUserVerifier = class { + } catch (e) { + if (e instanceof InitError) + this._reportInitError(e); +- else +- logErrorUnlessCancelled(e); ++ else if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) ++ logError(e); + } + + resolve(); +@@ -318,7 +318,10 @@ var ShellUserVerifier = class { async answerQuery(serviceName, answer) { // Wait for pending messages to be displayed before answering to // ensure no messages get lost @@ -1417,10 +2963,10 @@ index f1709edea3..17892c75e8 100644 + logError(e); + }); - this._authServicesSSSDSwitchable?.answerQuery(serviceName, answer); - this._authServicesLegacy?.answerQuery(serviceName, answer); + this._authServices.forEach(s => s.answerQuery(serviceName, answer)); + } diff --git a/js/ui/qrCode.js b/js/ui/qrCode.js -index 5920e45e5a..b53508c2dd 100644 +index 18c9b9977..9967d3f58 100644 --- a/js/ui/qrCode.js +++ b/js/ui/qrCode.js @@ -37,8 +37,12 @@ var QrCode = GObject.registerClass({ @@ -1451,55 +2997,100 @@ index 5920e45e5a..b53508c2dd 100644 } -- -2.51.0 +2.55.0 -From 147ad05d7d926478da5d35ea14bcec81bf0795ee Mon Sep 17 00:00:00 2001 +From 459447340040c89b04f408e6ecd9f23524b36742 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez -Date: Fri, 12 Dec 2025 12:58:59 +0100 -Subject: [PATCH 05/30] gdm: Change connectObject to connect +Date: Tue, 11 Aug 2026 00:50:38 +0200 +Subject: [PATCH 19/47] gdm: Change connectObject to connect connectObject is a helper that isn't available in this older version. --- - js/gdm/authPrompt.js | 24 +++--- - js/gdm/authServices.js | 138 +++++++++++++++++++++++++---------- - js/gdm/authServicesLegacy.js | 26 ++++++- - js/gdm/util.js | 61 +++++++++++----- - js/gdm/webLogin.js | 11 ++- - 5 files changed, 186 insertions(+), 74 deletions(-) + js/gdm/authList.js | 10 ++-- + js/gdm/authPrompt.js | 38 +++++++------ + js/gdm/authServices.js | 81 ++++++++++++++++++---------- + js/gdm/authServicesLegacy.js | 40 ++++++++++---- + js/gdm/authServicesSSSDSwitchable.js | 26 ++++++--- + js/gdm/loginDialog.js | 4 +- + js/gdm/userVerifier.js | 54 ++++++++++--------- + js/gdm/webLogin.js | 11 +++- + 8 files changed, 167 insertions(+), 97 deletions(-) +diff --git a/js/gdm/authList.js b/js/gdm/authList.js +index 5c7e1eaac..1761922f9 100644 +--- a/js/gdm/authList.js ++++ b/js/gdm/authList.js +@@ -45,8 +45,8 @@ var ItemIconPopup = class extends PopupMenu.PopupMenu { + item.add_child(labels); + this.addMenuItem(item); + +- sourceActor.connectObject('clicked', () => this.toggle(), this); +- sourceActor.connectObject('destroy', () => this.destroy(), this); ++ sourceActor.connect('clicked', () => this.toggle()); ++ sourceActor.connect('destroy', () => this.destroy()); + + this.actor.hide(); + +@@ -196,10 +196,8 @@ var AuthList = GObject.registerClass({ + this._scrollView.add_actor(this._box); + this._items = new Map(); + +- this._box.connectObject( +- 'child-added', () => this._updateItemsLayout(), +- 'child-removed', () => this._updateItemsLayout(), +- this); ++ this._box.connect('child-added', () => this._updateItemsLayout()); ++ this._box.connect('child-removed', () => this._updateItemsLayout()); + this.connect('key-focus-in', this._moveFocusToItems.bind(this)); + } + diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index 8c5c8a1fab..6996dac18b 100644 +index 4d239dde1..70199d42d 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -81,16 +81,16 @@ var AuthPrompt = GObject.registerClass({ +@@ -94,21 +94,25 @@ var AuthPrompt = GObject.registerClass({ - this._userVerifier = new GdmUtil.ShellUserVerifier(this._gdmClient, { reauthenticationOnly }); + this._userVerifier = new UserVerifier.ShellUserVerifier(this._gdmClient, { reauthenticationOnly }); - this._userVerifier.connectObject( -- 'ask-question', this._onAskQuestion.bind(this), -- '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), +- 'ask-question', (_, serviceName, question, secret) => +- this._onAskQuestion({serviceName, question, secret}), +- 'show-message', (_, serviceName, message, type, shouldWiggle) => +- this._onShowMessage({message, type, shouldWiggle}), +- 'show-choice-list', (_, serviceName, promptMessage, choiceList) => +- this._onShowChoiceList({serviceName, promptMessage, choiceList}), +- 'show-button', (_, args) => this._onShowButton(args), +- 'mechanisms-changed', (_, args) => this.emit('mechanisms-changed', args), +- 'web-login', (_, args) => this._onWebLogin(args), +- 'verification-failed', (_, serviceName, canRetry) => +- this._onVerificationFailed({serviceName, canRetry}), +- 'verification-complete', () => this._onVerificationComplete(), +- 'reset', (_, args) => this._onReset(args), - this); + this._userVerifierSignalIds = [ -+ this._userVerifier.connect('ask-question', this._onAskQuestion.bind(this)), -+ this._userVerifier.connect('show-message', this._onShowMessage.bind(this)), -+ this._userVerifier.connect('show-choice-list', this._onShowChoiceList.bind(this)), -+ this._userVerifier.connect('mechanisms-changed', (_, ...args) => this.emit('mechanisms-changed', ...args)), -+ this._userVerifier.connect('web-login', this._onWebLogin.bind(this)), -+ this._userVerifier.connect('verification-failed', this._onVerificationFailed.bind(this)), -+ this._userVerifier.connect('verification-complete', this._onVerificationComplete.bind(this)), -+ this._userVerifier.connect('reset', this._onReset.bind(this)), ++ this._userVerifier.connect( ++ 'ask-question', (_, serviceName, question, secret) => ++ this._onAskQuestion({serviceName, question, secret})), ++ this._userVerifier.connect( ++ 'show-message', (_, serviceName, message, type, shouldWiggle) => ++ this._onShowMessage({message, type, shouldWiggle})), ++ this._userVerifier.connect( ++ 'show-choice-list', (_, serviceName, promptMessage, choiceList) => ++ this._onShowChoiceList({serviceName, promptMessage, choiceList})), ++ this._userVerifier.connect('show-button', (_, args) => this._onShowButton(args)), ++ this._userVerifier.connect('mechanisms-changed', (_, args) => this.emit('mechanisms-changed', args)), ++ this._userVerifier.connect('web-login', (_, args) => this._onWebLogin(args)), ++ this._userVerifier.connect( ++ 'verification-failed', (_, serviceName, canRetry) => ++ this._onVerificationFailed({serviceName, canRetry})), ++ this._userVerifier.connect('verification-complete', () => this._onVerificationComplete()), ++ this._userVerifier.connect('reset', (_, args) => this._onReset(args)), + ]; this.connect('destroy', this._onDestroy.bind(this)); -@@ -134,7 +134,9 @@ var AuthPrompt = GObject.registerClass({ +@@ -152,7 +156,9 @@ var AuthPrompt = GObject.registerClass({ this._preemptiveAnswerWatchId = 0; } @@ -1511,19 +3102,18 @@ index 8c5c8a1fab..6996dac18b 100644 this._userVerifier = null; this._entry = null; diff --git a/js/gdm/authServices.js b/js/gdm/authServices.js -index fea7ba0634..88462cf17e 100644 +index 006209d39..a0c292c9f 100644 --- a/js/gdm/authServices.js +++ b/js/gdm/authServices.js -@@ -165,6 +165,8 @@ var AuthServices = GObject.registerClass({ +@@ -221,6 +221,7 @@ var AuthServices = GObject.registerClass({ destroy() { this.reset(); this.clear(); -+ this._disconnectManagers(); + this._handleDestroy(); this.emit('destroy'); } -@@ -202,10 +204,35 @@ var AuthServices = GObject.registerClass({ +@@ -260,10 +261,24 @@ var AuthServices = GObject.registerClass({ } _disconnectUserVerifierSignals() { @@ -1531,88 +3121,28 @@ index fea7ba0634..88462cf17e 100644 - this._userVerifier?.disconnectObject(this); - this._userVerifierChoiceList?.disconnectObject(this); - this._userVerifierCustomJSON?.disconnectObject(this); -+ this._userVerifierConnectionSignalIds?.forEach(id => -+ this._userVerifier?.get_connection().disconnect(id)); -+ this._userVerifierConnectionSignalIds = []; ++ if (this._userVerifierConnectionSignalId) { ++ this._userVerifier?.get_connection().disconnect(this._userVerifierConnectionSignalId); ++ this._userVerifierConnectionSignalId = 0; ++ } + + this._userVerifierSignalIds?.forEach(id => + this._userVerifier?.disconnect(id)); + this._userVerifierSignalIds = []; + -+ this._userVerifierChoiceListSignalIds?.forEach(id => -+ this._userVerifierChoiceList?.disconnect(id)); -+ this._userVerifierChoiceListSignalIds = []; ++ if (this._userVerifierChoiceListSignalId) { ++ this._userVerifierChoiceList?.disconnect(this._userVerifierChoiceListSignalId); ++ this._userVerifierChoiceListSignalId = 0; ++ } + -+ this._userVerifierCustomJSONSignalIds?.forEach(id => -+ this._userVerifierCustomJSON?.disconnect(id)); -+ this._userVerifierCustomJSONSignalIds = []; -+ } -+ -+ _disconnectManagers() { -+ this._smartcardManagerSignalIds?.forEach(id => -+ this._smartcardManager?.disconnect(id)); -+ this._smartcardManagerSignalIds = []; -+ -+ this._passkeyDeviceManagerSignalIds?.forEach(id => -+ this._passkeyDeviceManager?.disconnect(id)); -+ this._passkeyDeviceManagerSignalIds = []; -+ -+ this._fingerprintManagerSignalIds?.forEach(id => -+ this._fingerprintManager?.disconnect(id)); -+ this._fingerprintManagerSignalIds = []; ++ if (this._userVerifierCustomJSONSignalId) { ++ this._userVerifierCustomJSON?.disconnect(this._userVerifierCustomJSONSignalId); ++ this._userVerifierCustomJSONSignalId = 0; ++ } } _updateEnabledMechanisms() { -@@ -218,18 +245,24 @@ var AuthServices = GObject.registerClass({ - - _connectSmartcardManager() { - this._smartcardManager = SmartcardManager.getSmartcardManager(); -- this._smartcardManager.connectObject( -- 'smartcard-inserted', () => this._handleSmartcardChanged(), -- 'smartcard-removed', () => this._handleSmartcardChanged(), -- this); -+ this._smartcardManagerSignalIds = []; -+ let id = this._smartcardManager.connect('smartcard-inserted', -+ () => this._handleSmartcardChanged()); -+ this._smartcardManagerSignalIds.push(id); -+ id = this._smartcardManager.connect('smartcard-removed', -+ () => this._handleSmartcardChanged()); -+ this._smartcardManagerSignalIds.push(id); - } - - _connectPasskeyDeviceManager() { - this._passkeyDeviceManager = PasskeyDeviceManager.getPasskeyDeviceManager(); -- this._passkeyDeviceManager.connectObject( -- 'passkey-inserted', () => this._handlePasskeyChanged(), -- 'passkey-removed', () => this._handlePasskeyChanged(), -- this); -+ this._passkeyDeviceManagerSignalIds = []; -+ let id = this._passkeyDeviceManager.connect('passkey-inserted', -+ () => this._handlePasskeyChanged()); -+ this._passkeyDeviceManagerSignalIds.push(id); -+ id = this._passkeyDeviceManager.connect('passkey-removed', -+ () => this._handlePasskeyChanged()); -+ this._passkeyDeviceManagerSignalIds.push(id); - } - - _connectFingerprintManager() { -@@ -237,10 +270,11 @@ var AuthServices = GObject.registerClass({ - if (!this._reauthOnly) - return; - -- this._fingerprintManager = FingerprintManager.getFingerprintManager(); -- this._fingerprintManager.connectObject( -- 'reader-type-changed', () => this._handleFingerprintChanged(), -- this); -+ this._fingerprintManager = new FingerprintManager.FingerprintManager(); -+ this._fingerprintManagerSignalIds = []; -+ let id = this._fingerprintManager.connect('reader-type-changed', -+ () => this._handleFingerprintChanged()); -+ this._fingerprintManagerSignalIds.push(id); - } - - _waitPendingMessages() { -@@ -276,29 +310,53 @@ var AuthServices = GObject.registerClass({ +@@ -320,29 +335,39 @@ var AuthServices = GObject.registerClass({ } _connectUserVerifierSignals() { @@ -1639,57 +3169,43 @@ index fea7ba0634..88462cf17e 100644 - this._userVerifierCustomJSON?.connectObject( - 'request', (_, ...args) => this._onCustomJSONRequest(...args), - this); -+ this._userVerifierConnectionSignalIds = []; -+ let id = this._userVerifier.get_connection().connect('closed', -+ () => this._clearUserVerifier()); -+ this._userVerifierConnectionSignalIds.push(id); ++ this._userVerifierConnectionSignalId = this._userVerifier.get_connection().connect( ++ 'closed', () => this._clearUserVerifier()); + -+ this._userVerifierSignalIds = []; -+ id = this._userVerifier.connect('info', -+ (_, ...args) => this._onInfo(...args)); -+ this._userVerifierSignalIds.push(id); -+ id = this._userVerifier.connect('problem', -+ (_, ...args) => this._onProblem(...args)); -+ this._userVerifierSignalIds.push(id); -+ id = this._userVerifier.connect('info-query', -+ (_, ...args) => this._onInfoQuery(...args)); -+ this._userVerifierSignalIds.push(id); -+ id = this._userVerifier.connect('secret-info-query', -+ (_, ...args) => this._onSecretInfoQuery(...args)); -+ this._userVerifierSignalIds.push(id); -+ id = this._userVerifier.connect('conversation-started', -+ (_, ...args) => this._onConversationStarted(...args)); -+ this._userVerifierSignalIds.push(id); -+ id = this._userVerifier.connect('conversation-stopped', -+ (_, ...args) => this._onConversationStopped(...args)); -+ this._userVerifierSignalIds.push(id); -+ id = this._userVerifier.connect('service-unavailable', -+ (_, ...args) => this._onServiceUnavailable(...args)); -+ this._userVerifierSignalIds.push(id); -+ id = this._userVerifier.connect('reset', -+ () => this.emit('reset', {})); -+ this._userVerifierSignalIds.push(id); -+ id = this._userVerifier.connect('verification-complete', -+ (_, ...args) => this._onVerificationComplete(...args)); -+ this._userVerifierSignalIds.push(id); ++ this._userVerifierSignalIds = [ ++ this._userVerifier.connect('info', ++ (_, ...args) => this._onInfo(...args)), ++ this._userVerifier.connect('problem', ++ (_, ...args) => this._onProblem(...args)), ++ this._userVerifier.connect('info-query', ++ (_, ...args) => this._onInfoQuery(...args)), ++ this._userVerifier.connect('secret-info-query', ++ (_, ...args) => this._onSecretInfoQuery(...args)), ++ this._userVerifier.connect('conversation-started', ++ (_, ...args) => this._onConversationStarted(...args)), ++ this._userVerifier.connect('conversation-stopped', ++ (_, ...args) => this._onConversationStopped(...args)), ++ this._userVerifier.connect('service-unavailable', ++ (_, ...args) => this._onServiceUnavailable(...args)), ++ this._userVerifier.connect('reset', ++ () => this.emit('reset', {})), ++ this._userVerifier.connect('verification-complete', ++ (_, ...args) => this._onVerificationComplete(...args)), ++ ]; + -+ this._userVerifierChoiceListSignalIds = []; + if (this._userVerifierChoiceList) { -+ id = this._userVerifierChoiceList.connect('choice-query', -+ (_, ...args) => this._onChoiceListQuery(...args)); -+ this._userVerifierChoiceListSignalIds.push(id); ++ this._userVerifierChoiceListSignalId = this._userVerifierChoiceList.connect( ++ 'choice-query', (_, ...args) => this._onChoiceListQuery(...args)); + } + -+ this._userVerifierCustomJSONSignalIds = []; + if (this._userVerifierCustomJSON) { -+ id = this._userVerifierCustomJSON.connect('request', -+ (_, ...args) => this._onCustomJSONRequest(...args)); -+ this._userVerifierCustomJSONSignalIds.push(id); ++ this._userVerifierCustomJSONSignalId = this._userVerifierCustomJSON.connect( ++ 'request', (_, ...args) => this._onCustomJSONRequest(...args)); + } } _onInfo(serviceName, info) { -@@ -456,6 +514,8 @@ var AuthServices = GObject.registerClass({ +@@ -504,6 +529,8 @@ var AuthServices = GObject.registerClass({ _handleClear() {} @@ -1699,154 +3215,244 @@ index fea7ba0634..88462cf17e 100644 _handleUpdateEnabledMechanisms() { diff --git a/js/gdm/authServicesLegacy.js b/js/gdm/authServicesLegacy.js -index 0fd0e10d81..5955aeadff 100644 +index 1b336e9d1..0592a6485 100644 --- a/js/gdm/authServicesLegacy.js +++ b/js/gdm/authServicesLegacy.js -@@ -50,6 +50,7 @@ var AuthServicesLegacy = GObject.registerClass({ +@@ -68,6 +68,7 @@ var AuthServicesLegacy = GObject.registerClass({ this._updateEnabledMechanisms(); this._credentialManagers = {}; -+ this._credentialManagerSignalIds = {}; ++ this._credentialManagerSignalIds = new Map(); this.addCredentialManager(OVirt.SERVICE_NAME, OVirt.getOVirtCredentialsManager()); this.addCredentialManager(Vmware.SERVICE_NAME, Vmware.getVmwareCredentialsManager()); -@@ -104,6 +105,20 @@ var AuthServicesLegacy = GObject.registerClass({ - this._selectedMechanism = null; +@@ -382,6 +383,20 @@ var AuthServicesLegacy = GObject.registerClass({ + return credentialManager && credentialManager.token !== null; } + _handleDestroy() { -+ this._disconnectCredentialManagers(); ++ this._smartcardManagerSignalIds?.forEach(id => ++ this._smartcardManager?.disconnect(id)); ++ this._smartcardManagerSignalIds = []; ++ ++ this._fingerprintManagerSignalIds?.forEach(id => ++ this._fingerprintManager?.disconnect(id)); ++ this._fingerprintManagerSignalIds = []; ++ ++ this._credentialManagerSignalIds.forEach((id, serviceName) => ++ this._credentialManagers[serviceName]?.disconnect(id)); ++ this._credentialManagerSignalIds.clear(); + } + -+ _disconnectCredentialManagers() { -+ Object.keys(this._credentialManagerSignalIds).forEach(serviceName => { -+ const signalId = this._credentialManagerSignalIds[serviceName]; -+ const credentialManager = this._credentialManagers[serviceName]; -+ if (signalId) -+ credentialManager?.disconnect(signalId); -+ }); -+ this._credentialManagerSignalIds = {}; -+ } -+ - _handleClear() { - this._smartcardInProgress = false; - this._clearFingerprintReadyTimeout(); -@@ -359,9 +374,9 @@ var AuthServicesLegacy = GObject.registerClass({ + addCredentialManager(serviceName, credentialManager) { + if (this._credentialManagers[serviceName]) + return; +@@ -390,9 +405,9 @@ var AuthServicesLegacy = GObject.registerClass({ if (credentialManager.token) this._onCredentialManagerAuthenticated(credentialManager); - credentialManager.connectObject( - 'user-authenticated', () => this._onCredentialManagerAuthenticated(credentialManager), - this); -+ const id = credentialManager.connect('user-authenticated', -+ () => this._onCredentialManagerAuthenticated(credentialManager)); -+ this._credentialManagerSignalIds[serviceName] = id; ++ this._credentialManagerSignalIds.set(serviceName, ++ credentialManager.connect('user-authenticated', ++ () => this._onCredentialManagerAuthenticated(credentialManager))); } removeCredentialManager(serviceName) { -@@ -369,7 +384,10 @@ var AuthServicesLegacy = GObject.registerClass({ +@@ -400,7 +415,10 @@ var AuthServicesLegacy = GObject.registerClass({ if (!credentialManager) return; - credentialManager.disconnectObject(this); -+ const signalId = this._credentialManagerSignalIds[serviceName]; -+ if (signalId) -+ credentialManager.disconnect(signalId); -+ delete this._credentialManagerSignalIds[serviceName]; ++ const signalId = this._credentialManagerSignalIds.get(serviceName); ++ credentialManager.disconnect(signalId); ++ this._credentialManagerSignalIds.delete(serviceName); ++ delete this._credentialManagers[serviceName]; if (this._selectedMechanism?.serviceName === serviceName) -diff --git a/js/gdm/util.js b/js/gdm/util.js -index 17892c75e8..85de597d33 100644 ---- a/js/gdm/util.js -+++ b/js/gdm/util.js -@@ -270,6 +270,8 @@ var ShellUserVerifier = class { +@@ -409,10 +427,10 @@ var AuthServicesLegacy = GObject.registerClass({ + + _connectSmartcardManager() { + this._smartcardManager = SmartcardManager.getSmartcardManager(); +- this._smartcardManager.connectObject( +- 'smartcard-inserted', () => this._onSmartcardChanged(), +- 'smartcard-removed', () => this._onSmartcardChanged(), +- this); ++ this._smartcardManagerSignalIds = [ ++ this._smartcardManager.connect('smartcard-inserted', () => this._onSmartcardChanged()), ++ this._smartcardManager.connect('smartcard-removed', () => this._onSmartcardChanged()), ++ ]; + } + + _connectFingerprintManager() { +@@ -420,9 +438,9 @@ var AuthServicesLegacy = GObject.registerClass({ + return; + + this._fingerprintManager = FingerprintManager.getFingerprintManager(); +- this._fingerprintManager.connectObject( +- 'reader-type-changed', () => this._onFingerprintChanged(), +- this); ++ this._fingerprintManagerSignalIds = [ ++ this._fingerprintManager.connect('reader-type-changed', () => this._onFingerprintChanged()), ++ ]; + } + + _onSmartcardChanged() { +diff --git a/js/gdm/authServicesSSSDSwitchable.js b/js/gdm/authServicesSSSDSwitchable.js +index 9625b5e82..8ec9220d0 100644 +--- a/js/gdm/authServicesSSSDSwitchable.js ++++ b/js/gdm/authServicesSSSDSwitchable.js +@@ -594,20 +594,30 @@ var AuthServicesSSSDSwitchable = GObject.registerClass({ + this._webLoginTimeoutId = 0; + } + ++ _handleDestroy() { ++ this._smartcardManagerSignalIds?.forEach(id => ++ this._smartcardManager?.disconnect(id)); ++ this._smartcardManagerSignalIds = []; ++ ++ this._fido2TokenManagerSignalIds?.forEach(id => ++ this._fido2TokenManager?.disconnect(id)); ++ this._fido2TokenManagerSignalIds = []; ++ } ++ + _connectSmartcardManager() { + this._smartcardManager = SmartcardManager.getSmartcardManager(); +- this._smartcardManager.connectObject( +- 'smartcard-inserted', () => this._onSmartcardChanged(), +- 'smartcard-removed', () => this._onSmartcardChanged(), +- this); ++ this._smartcardManagerSignalIds = [ ++ this._smartcardManager.connect('smartcard-inserted', () => this._onSmartcardChanged()), ++ this._smartcardManager.connect('smartcard-removed', () => this._onSmartcardChanged()), ++ ]; + } + + _connectFido2TokenManager() { + this._fido2TokenManager = Fido2TokenManager.getFido2TokenManager(); +- this._fido2TokenManager.connectObject( +- 'fido2-token-inserted', () => this._onFido2TokenChanged(), +- 'fido2-token-removed', () => this._onFido2TokenChanged(), +- this); ++ this._fido2TokenManagerSignalIds = [ ++ this._fido2TokenManager.connect('fido2-token-inserted', () => this._onFido2TokenChanged()), ++ this._fido2TokenManager.connect('fido2-token-removed', () => this._onFido2TokenChanged()), ++ ]; + } + + _onSmartcardChanged() { +diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js +index cb62847ad..efd33581c 100644 +--- a/js/gdm/loginDialog.js ++++ b/js/gdm/loginDialog.js +@@ -367,8 +367,8 @@ var 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.connect('notify::verification-status', ++ () => this._updateCancelButton()); + this._authPrompt.hide(); + this.add_child(this._authPrompt); + +diff --git a/js/gdm/userVerifier.js b/js/gdm/userVerifier.js +index 8a29d7a13..cba5c20cb 100644 +--- a/js/gdm/userVerifier.js ++++ b/js/gdm/userVerifier.js +@@ -118,6 +118,7 @@ var ShellUserVerifier = class { + this._client = client; + this._cancellable = null; + this._authServices = []; ++ this._authServicesSignalIds = []; + this._driverService = null; + + this._beginPromise = null; +@@ -291,19 +292,20 @@ var ShellUserVerifier = class { + _setUserVerifier(userVerifier) { + this._clearUserVerifier(); + this._userVerifier = userVerifier; +- this._userVerifier.get_connection().connectObject( +- 'closed', () => this._clearUserVerifier(), +- this); ++ this._userVerifierConnectionSignalId = this._userVerifier.get_connection().connect( ++ 'closed', () => this._clearUserVerifier()); + } + + _clearUserVerifier() { +- this._userVerifier?.get_connection().disconnectObject(this); ++ if (this._userVerifierConnectionSignalId) { ++ this._userVerifier?.get_connection().disconnect(this._userVerifierConnectionSignalId); ++ this._userVerifierConnectionSignalId = 0; ++ } + this._userVerifier = null; + } + + destroy() { +- this._authServices.forEach(s => s.destroy()); +- this._authServices = []; ++ this._clearAuthServices(); this.cancel(); -+ this._clearAuthServices(); -+ - this._settings.run_dispose(); - this._settings = null; - } -@@ -498,31 +500,54 @@ var ShellUserVerifier = class { +@@ -531,29 +533,31 @@ var ShellUserVerifier = class { } _clearAuthServices() { -+ this._authServicesSSSDSwitchableSignalIds?.forEach(id => -+ this._authServicesSSSDSwitchable?.disconnect(id)); -+ this._authServicesSSSDSwitchableSignalIds = []; - this._authServicesSSSDSwitchable?.destroy(); - this._authServicesSSSDSwitchable = null; -+ -+ this._authServicesLegacySignalIds?.forEach(id => -+ this._authServicesLegacy?.disconnect(id)); -+ this._authServicesLegacySignalIds = []; - this._authServicesLegacy?.destroy(); - this._authServicesLegacy = null; +- this._authServices.forEach(s => s.destroy()); ++ this._authServices.forEach((s, i) => { ++ this._authServicesSignalIds[i]?.forEach(id => s.disconnect(id)); ++ s.destroy(); ++ }); + this._authServices = []; ++ this._authServicesSignalIds = []; } _connectAuthServices() { -- [ -- this._authServicesSSSDSwitchable, -- 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), -- 'web-login', (_, ...args) => this.emit('web-login', ...args), +- this._authServices.forEach(authServices => { +- authServices.connectObject( +- 'ask-question', (_, args) => +- this.emit('ask-question', args.serviceName, args.question, args.secret ?? false), +- '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.serviceName, args.promptMessage, args.choiceList), +- 'show-button', (_, args) => this.emit('show-button', args), +- 'mechanisms-changed', () => this._onMechanismsChanged().catch(logError), +- 'web-login', (_, args) => this.emit('web-login', args), - this); - }); -+ const connectSignals = authServices => { -+ if (!authServices) -+ return []; -+ -+ return [ -+ authServices.connect('ask-question', -+ (_, ...args) => this.emit('ask-question', ...args)), -+ authServices.connect('queue-message', -+ (_, ...args) => this._queueMessage(...args)), -+ authServices.connect('queue-priority-message', -+ (_, ...args) => this._queuePriorityMessage(...args)), -+ authServices.connect('wait-pending-messages', -+ (_, ...args) => this._waitPendingMessages(...args)), -+ authServices.connect('filter-messages', -+ (_, ...args) => this._filterServiceMessages(...args)), -+ authServices.connect('verification-failed', -+ (_, ...args) => this._verificationFailed(...args)), -+ authServices.connect('verification-complete', -+ (_, ...args) => this.emit('verification-complete', ...args)), -+ authServices.connect('reset', -+ (_, ...args) => this.emit('reset', ...args)), -+ authServices.connect('show-choice-list', -+ (_, ...args) => this.emit('show-choice-list', ...args)), -+ authServices.connect('mechanisms-changed', -+ (_, ...args) => this._onMechanismsChanged(...args)), -+ authServices.connect('web-login', -+ (_, ...args) => this.emit('web-login', ...args)), -+ ]; -+ }; -+ -+ this._authServicesSSSDSwitchableSignalIds = -+ connectSignals(this._authServicesSSSDSwitchable); -+ this._authServicesLegacySignalIds = -+ connectSignals(this._authServicesLegacy); ++ this._authServicesSignalIds = this._authServices.map(authServices => [ ++ authServices.connect('ask-question', (_, args) => ++ this.emit('ask-question', args.serviceName, args.question, args.secret ?? false)), ++ authServices.connect('queue-message', (_, args) => this._queueMessage(args)), ++ authServices.connect('queue-priority-message', (_, args) => this._queuePriorityMessage(args)), ++ authServices.connect('wait-pending-messages', (_, args) => this._waitPendingMessages(args)), ++ authServices.connect('filter-messages', (_, args) => this._filterServiceMessages(args)), ++ authServices.connect('verification-failed', (_, args) => this._verificationFailed(args)), ++ authServices.connect('verification-complete', () => this.emit('verification-complete')), ++ authServices.connect('reset', (_, args) => this.emit('reset', args)), ++ authServices.connect('show-choice-list', (_, args) => ++ this.emit('show-choice-list', args.serviceName, args.promptMessage, args.choiceList)), ++ authServices.connect('show-button', (_, args) => this.emit('show-button', args)), ++ authServices.connect('mechanisms-changed', () => this._onMechanismsChanged().catch(logError)), ++ authServices.connect('web-login', (_, args) => this.emit('web-login', args)), ++ ]); } - _verificationFailed(serviceName, canRetry) { + _verificationFailed({serviceName, canRetry}) { diff --git a/js/gdm/webLogin.js b/js/gdm/webLogin.js -index 9c858f7269..26da94b624 100644 +index e3fb95d59..9304332ac 100644 --- a/js/gdm/webLogin.js +++ b/js/gdm/webLogin.js -@@ -182,7 +182,14 @@ var WebLoginDialog = GObject.registerClass({ +@@ -178,7 +178,14 @@ var WebLoginDialog = GObject.registerClass({ } _updateButtons(buttons) { @@ -1861,7 +3467,7 @@ index 9c858f7269..26da94b624 100644 this._cancelButton = this._addButton({ label: _('Cancel'), action: () => this.emit('cancel'), -@@ -202,11 +209,11 @@ var WebLoginDialog = GObject.registerClass({ +@@ -199,11 +206,11 @@ var WebLoginDialog = GObject.registerClass({ }), }); @@ -1876,29 +3482,27 @@ index 9c858f7269..26da94b624 100644 button.default = b.default; -- -2.51.0 +2.55.0 -From bcf392cbdac51d1bb1fb2e161c674bc4bd3286a6 Mon Sep 17 00:00:00 2001 +From 2892973a8bf876e6eaf7879cfa3e2547b5e1a3e9 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez -Date: Fri, 26 Dec 2025 15:12:16 +0100 -Subject: [PATCH 06/30] gdm: Use promisify adding two params +Date: Tue, 11 Aug 2026 00:52:12 +0200 +Subject: [PATCH 20/47] gdm: Use promisify adding two params This is how it works in this old version --- - js/gdm/authServices.js | 12 +++++------- - js/gdm/util.js | 7 ++++++- - js/gdm/webLogin.js | 2 +- + js/gdm/authServices.js | 14 +++++++------- js/ui/qrCode.js | 2 +- - 4 files changed, 13 insertions(+), 10 deletions(-) + 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/js/gdm/authServices.js b/js/gdm/authServices.js -index 88462cf17e..3cb1985817 100644 +index a0c292c9f..4fea1b1ad 100644 --- a/js/gdm/authServices.js +++ b/js/gdm/authServices.js -@@ -9,13 +9,11 @@ const PasskeyDeviceManager = imports.misc.passkeyDeviceManager; - const SmartcardManager = imports.misc.smartcardManager; - const Util = imports.gdm.util; +@@ -5,13 +5,13 @@ const { Gdm, Gio, GLib, GObject } = imports.gi; + const Params = imports.misc.params; + const UserVerifier = imports.gdm.userVerifier; -Gio._promisify(Gdm.Client.prototype, 'open_reauthentication_channel'); -Gio._promisify(Gdm.Client.prototype, 'get_user_verifier'); @@ -1907,54 +3511,18 @@ index 88462cf17e..3cb1985817 100644 -Gio._promisify(Gdm.UserVerifierProxy.prototype, 'call_answer_query'); -Gio._promisify(Gdm.UserVerifierChoiceListProxy.prototype, 'call_select_choice'); -Gio._promisify(Gdm.UserVerifierCustomJSONProxy.prototype, 'call_reply'); ++Gio._promisify(Gdm.Client.prototype, 'open_reauthentication_channel', 'open_reauthentication_channel_finish'); ++Gio._promisify(Gdm.Client.prototype, 'get_user_verifier', 'get_user_verifier_finish'); +Gio._promisify(Gdm.UserVerifierProxy.prototype, 'call_begin_verification_for_user', 'call_begin_verification_for_user_finish'); +Gio._promisify(Gdm.UserVerifierProxy.prototype, 'call_begin_verification', 'call_begin_verification_finish'); +Gio._promisify(Gdm.UserVerifierProxy.prototype, 'call_answer_query', 'call_answer_query_finish'); +Gio._promisify(Gdm.UserVerifierChoiceListProxy.prototype, 'call_select_choice', 'call_select_choice_finish'); +Gio._promisify(Gdm.UserVerifierCustomJSONProxy.prototype, 'call_reply', 'call_reply_finish'); - var AuthServices = GObject.registerClass({ - Signals: { -diff --git a/js/gdm/util.js b/js/gdm/util.js -index 85de597d33..4aeec5b02a 100644 ---- a/js/gdm/util.js -+++ b/js/gdm/util.js -@@ -3,7 +3,7 @@ - DISABLE_USER_LIST_KEY, fadeInActor, fadeOutActor, cloneAndFadeOutActor, - InitError, isSelectable, getIconName */ - --const { Clutter, Gio, GLib } = imports.gi; -+const { Clutter, Gdm, Gio, GLib } = imports.gi; - const Signals = imports.signals; - - const Batch = imports.gdm.batch; -@@ -13,6 +13,11 @@ const Params = imports.misc.params; - const { AuthServicesLegacy } = imports.gdm.authServicesLegacy; - const { AuthServicesSSSDSwitchable } = imports.gdm.authServicesSSSDSwitchable; - -+Gio._promisify(Gdm.Client.prototype, 'open_reauthentication_channel', 'open_reauthentication_channel_finish'); -+Gio._promisify(Gdm.Client.prototype, 'get_user_verifier', 'get_user_verifier_finish'); -+Gio._promisify(Gdm.UserVerifierProxy.prototype, 'call_begin_verification_for_user', 'call_begin_verification_for_user_finish'); -+Gio._promisify(Gdm.UserVerifierProxy.prototype, 'call_begin_verification', 'call_begin_verification_finish'); -+ - var FADE_ANIMATION_TIME = 160; - var CLONE_FADE_ANIMATION_TIME = 250; - -diff --git a/js/gdm/webLogin.js b/js/gdm/webLogin.js -index 26da94b624..f4972144fa 100644 ---- a/js/gdm/webLogin.js -+++ b/js/gdm/webLogin.js -@@ -9,7 +9,7 @@ const Params = imports.misc.params; - const { Spinner } = imports.ui.animation; - const { QrCode } = imports.ui.qrCode; - --Gio._promisify(GnomeQR, 'generate_qr_code_async'); -+Gio._promisify(GnomeQR, 'generate_qr_code_async', 'generate_qr_code_finish'); - - const QR_CODE_SIZE = 150; - const WEB_LOGIN_SPINNER_SIZE = 35; + var Role = { + PASSWORD: 'password', diff --git a/js/ui/qrCode.js b/js/ui/qrCode.js -index b53508c2dd..547ad7d153 100644 +index 9967d3f58..bc73d337d 100644 --- a/js/ui/qrCode.js +++ b/js/ui/qrCode.js @@ -2,7 +2,7 @@ @@ -1967,107 +3535,22 @@ index b53508c2dd..547ad7d153 100644 const QR_CODE_DEFAULT_SIZE = 150; const QR_CODE_TRANSPARENT_COLOR = new GnomeQR.Color({alpha: 0}); -- -2.51.0 +2.55.0 -From 74a94cb0c34413d45ef2e1d599822c25c434e1b8 Mon Sep 17 00:00:00 2001 +From dca90d3062616e0b5e050fe3697e0453a7d886d9 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez -Date: Fri, 27 Mar 2026 13:52:50 +0100 -Subject: [PATCH 07/30] gdm: Use timeout_add_seconds instead of once +Date: Tue, 11 Aug 2026 00:53:33 +0200 +Subject: [PATCH 21/47] data/theme: Add new icons to theme gresource -_once type doesn't exist in this old glib version ---- - js/gdm/authServices.js | 7 +++++-- - js/gdm/authServicesSSSDSwitchable.js | 3 ++- - 2 files changed, 7 insertions(+), 3 deletions(-) - -diff --git a/js/gdm/authServices.js b/js/gdm/authServices.js -index 3cb1985817..003b4836a6 100644 ---- a/js/gdm/authServices.js -+++ b/js/gdm/authServices.js -@@ -277,8 +277,11 @@ var AuthServices = GObject.registerClass({ - - _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/authServicesSSSDSwitchable.js b/js/gdm/authServicesSSSDSwitchable.js -index 601f4cd295..48c1c020fb 100644 ---- a/js/gdm/authServicesSSSDSwitchable.js -+++ b/js/gdm/authServicesSSSDSwitchable.js -@@ -195,7 +195,7 @@ var AuthServicesSSSDSwitchable = GObject.registerClass({ - 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; -@@ -203,6 +203,7 @@ var AuthServicesSSSDSwitchable = GObject.registerClass({ - this.emit('reset', {softReset: true}); - - this._webLoginTimeoutId = 0; -+ return GLib.SOURCE_REMOVE; - }); - } - --- -2.51.0 - - -From aff9353c2f80c15111f9b7f772b5fa45277251fd Mon Sep 17 00:00:00 2001 -From: Joan Torres Lopez -Date: Fri, 27 Mar 2026 13:56:56 +0100 -Subject: [PATCH 08/30] gdm: fix Promise.withResolvers - -Use manual new Promise to extract them. ---- - 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 003b4836a6..00d19004f3 100644 ---- a/js/gdm/authServices.js -+++ b/js/gdm/authServices.js -@@ -283,7 +283,11 @@ var AuthServices = GObject.registerClass({ - return GLib.SOURCE_REMOVE; - }); - -- const {promise, resolve, reject} = Promise.withResolvers(); -+ let resolve, reject; -+ const promise = new Promise((_resolve, _reject) => { -+ resolve = _resolve; -+ reject = _reject; -+ }); - const task = Gio.Task.new(this, cancellable, () => { - try { - const res = task.propagate_boolean(); --- -2.51.0 - - -From fdad9ac3e5bb6699d835d23a50033244fefc6907 Mon Sep 17 00:00:00 2001 -From: Joan Torres Lopez -Date: Fri, 12 Dec 2025 19:33:38 +0100 -Subject: [PATCH 09/30] data/theme: Add new icons to theme gresource - -They were in a different icons gresource that doesn't exist in 40 vesion +They were in a different icons gresource that doesn't exist in 40 version --- data/gnome-shell-theme.gresource.xml | 2 ++ data/icons/meson.build | 1 + 2 files changed, 3 insertions(+) diff --git a/data/gnome-shell-theme.gresource.xml b/data/gnome-shell-theme.gresource.xml -index 7857f4407b..4f697f88df 100644 +index 7857f4407..4f697f88d 100644 --- a/data/gnome-shell-theme.gresource.xml +++ b/data/gnome-shell-theme.gresource.xml @@ -4,6 +4,8 @@ @@ -2080,20 +3563,20 @@ index 7857f4407b..4f697f88df 100644 checkbox-off-focused.svg checkbox-off.svg diff --git a/data/icons/meson.build b/data/icons/meson.build -index eff6e4b530..44e2b164a3 100644 +index eff6e4b53..44e2b164a 100644 --- a/data/icons/meson.build +++ b/data/icons/meson.build @@ -1 +1,2 @@ install_subdir('hicolor', install_dir: icondir) +install_subdir('scalable', install_dir: icondir) -- -2.51.0 +2.55.0 -From e0b648ce306e29ecb754711700d0b95aa4ed8769 Mon Sep 17 00:00:00 2001 +From 38d5784a618dea5b90ea5eb1accfb2cb63e0c688 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez -Date: Mon, 15 Dec 2025 13:34:27 +0100 -Subject: [PATCH 10/30] gdm/authmenuButton: Use None ornament +Date: Tue, 11 Aug 2026 00:53:52 +0200 +Subject: [PATCH 22/47] gdm/authmenuButton: Use None ornament NOT_DOT doesn't exist. --- @@ -2101,10 +3584,10 @@ NOT_DOT doesn't exist. 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/gdm/authMenuButton.js b/js/gdm/authMenuButton.js -index e58c331b45..9a02b0cb05 100644 +index abb10e679..6dabf0f86 100644 --- a/js/gdm/authMenuButton.js +++ b/js/gdm/authMenuButton.js -@@ -236,7 +236,7 @@ var AuthMenuButton = GObject.registerClass({ +@@ -171,7 +171,7 @@ var AuthMenuButton = GObject.registerClass({ _updateOrnament() { for (const menuItem of this._items.values()) @@ -2112,80 +3595,51 @@ index e58c331b45..9a02b0cb05 100644 + menuItem.setOrnament(PopupMenu.Ornament.NONE); for (const itemKey of this._activeItems) { - const menuItem = this._getMenuItem(JSON.parse(itemKey)); + const menuItem = this._items.get(itemKey); -- -2.51.0 +2.55.0 -From 6a246865fb5a2da8724aafea34995d5aab81c263 Mon Sep 17 00:00:00 2001 +From 6a788b8bf0ef69ecf21c0996b7eed244a9adbf55 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez -Date: Fri, 12 Dec 2025 20:33:15 +0100 -Subject: [PATCH 11/30] gdm/authMenuButton: Add proper style classes to button +Date: Tue, 11 Aug 2026 00:54:22 +0200 +Subject: [PATCH 23/47] gdm/authMenuButton: Add proper style classes to button --- 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 9a02b0cb05..64dcf1a3f4 100644 +index 6dabf0f86..1f98e8f3a 100644 --- a/js/gdm/authMenuButton.js +++ b/js/gdm/authMenuButton.js -@@ -146,7 +146,7 @@ var AuthMenuButton = GObject.registerClass({ - params.sectionOrder ??= []; - super._init({ +@@ -136,7 +136,7 @@ var AuthMenuButton = GObject.registerClass({ + icon_name: 'cog-wheel-symbolic', + sectionOrder: [], ...params, - style_class: 'login-dialog-button login-dialog-auth-menu-button', + style_class: 'modal-dialog-button button login-dialog-auth-menu-button', - reactive: true, - track_hover: true, can_focus: true, + accessible_role: Atk.Role.MENU, + button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE, -- -2.51.0 +2.55.0 -From 11ad6201e3c032674d4e58f44d9fea9820fb4234 Mon Sep 17 00:00:00 2001 +From c9763c37dc7745b904b350b47e47eaac4f7fe34a Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez -Date: Fri, 2 Jan 2026 14:48:42 +0100 -Subject: [PATCH 12/30] gdm/authMenuButton: Add style to indicator description - -This is needed to set its color in the lockscreen. ---- - data/theme/gnome-shell-sass/widgets/_login-dialog.scss | 4 ++++ - 1 file changed, 4 insertions(+) - -diff --git a/data/theme/gnome-shell-sass/widgets/_login-dialog.scss b/data/theme/gnome-shell-sass/widgets/_login-dialog.scss -index f3aeed4bf3..29c6803082 100644 ---- a/data/theme/gnome-shell-sass/widgets/_login-dialog.scss -+++ b/data/theme/gnome-shell-sass/widgets/_login-dialog.scss -@@ -123,6 +123,10 @@ $_gdm_fg: $osd_fg_color; - } - } - -+.login-dialog-auth-menu-button-indicator-description { -+ color: $osd_fg_color; -+} -+ - .login-dialog-bottom-button-group { - padding: 32px; - spacing: 16px; --- -2.51.0 - - -From d7c9b074f8cb0bf2295daa3a518776cae023a039 Mon Sep 17 00:00:00 2001 -From: Joan Torres Lopez -Date: Fri, 27 Mar 2026 17:57:03 +0100 -Subject: [PATCH 13/30] gdm/authMenuButton: Add no-op () instead of null +Date: Tue, 11 Aug 2026 00:54:52 +0200 +Subject: [PATCH 24/47] gdm/authMenuButton: Add no-op () instead of null --- 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 64dcf1a3f4..a211ebe0d8 100644 +index 1f98e8f3a..18612975c 100644 --- a/js/gdm/authMenuButton.js +++ b/js/gdm/authMenuButton.js -@@ -475,7 +475,7 @@ class AuthMenuButtonIndicator extends AuthMenuButton { +@@ -450,7 +450,7 @@ class AuthMenuButtonIndicator extends AuthMenuButton { this._descriptionLabel, 'visible', GObject.BindingFlags.SYNC_CREATE, (bind, source) => [true, !!source], @@ -2193,15 +3647,15 @@ index 64dcf1a3f4..a211ebe0d8 100644 + () => {}); container.add_child(this._descriptionLabel); - return container; + this.child = container; -- -2.51.0 +2.55.0 -From 5fb0f23fda55d433a08db68920ed896cdf9e3d08 Mon Sep 17 00:00:00 2001 +From 1f38ac8cb8d83f6f8442bb68f310dc12045e2cb4 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez -Date: Thu, 2 Apr 2026 19:38:34 +0200 -Subject: [PATCH 14/30] gdm/authMenuButton: Ensure visible of description is +Date: Tue, 11 Aug 2026 01:01:30 +0200 +Subject: [PATCH 25/47] gdm/authMenuButton: Ensure visible of description is properly binded to its text @@ -2210,10 +3664,10 @@ its text 1 file changed, 1 insertion(+) diff --git a/js/gdm/authMenuButton.js b/js/gdm/authMenuButton.js -index a211ebe0d8..3dafd87630 100644 +index 18612975c..8f2afdfca 100644 --- a/js/gdm/authMenuButton.js +++ b/js/gdm/authMenuButton.js -@@ -510,6 +510,7 @@ class AuthMenuButtonIndicator extends AuthMenuButton { +@@ -492,6 +492,7 @@ class AuthMenuButtonIndicator extends AuthMenuButton { updateDescriptionLabel() { const [item] = this._items.size === 1 ? this.getItems() : []; this._descriptionLabel.text = item?.description ?? ''; @@ -2222,79 +3676,208 @@ index a211ebe0d8..3dafd87630 100644 // Override to force visibility even when there's only one item -- -2.51.0 +2.55.0 -From e9abaf90b24df1f98108ab48c5d00ec87a6d5c9e Mon Sep 17 00:00:00 2001 +From 6225791e1d3fdc71f30a562a5eed7a2015a9f6e8 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez -Date: Fri, 27 Mar 2026 13:38:06 +0100 -Subject: [PATCH 15/30] Fix foreach in wrong type +Date: Tue, 11 Aug 2026 11:29:46 +0200 +Subject: [PATCH 26/47] gdm/authMenuButton: Don't use &&= -Map.keys() returns an iterator which doesn't have .forEach() in older -SpiderMonkey. Wrapped it with [...] to convert to an array first. +It's not supported in this version --- 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 3dafd87630..393dc5da4b 100644 +index 8f2afdfca..40b777b5f 100644 --- a/js/gdm/authMenuButton.js +++ b/js/gdm/authMenuButton.js -@@ -307,7 +307,7 @@ var AuthMenuButton = GObject.registerClass({ - this._items.delete(itemKey); - }); +@@ -320,7 +320,7 @@ var AuthMenuButton = GObject.registerClass({ + } -- this._sections.keys().forEach(sectionName => { -+ [...this._sections.keys()].forEach(sectionName => { - const itemsInSection = this._findItems({sectionName}); - if (itemsInSection.length === 0) { - const section = this._sections.get(sectionName); + updateVisibility({visible = true} = {}) { +- visible &&= this._canBeVisible(); ++ visible = visible && this._canBeVisible(); + + if (this._targetVisibility === visible) + return; -- -2.51.0 +2.55.0 -From 2e28cf05ef0033c42c6bb54839184701718bf48a Mon Sep 17 00:00:00 2001 +From 408d7e56e0698ac3a9bd915a96fd273a60fd72bb Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez -Date: Fri, 27 Mar 2026 12:56:24 +0100 -Subject: [PATCH 16/30] Change ??= by if statement +Date: Tue, 11 Aug 2026 01:21:50 +0200 +Subject: [PATCH 27/47] gdm/authMenuButton: Use connect with notify::text + instead of bind_property_full -This old js version doesn't support it. +bind_property_full is not available in this old version. --- - js/gdm/authMenuButton.js | 6 ++++-- - 1 file changed, 4 insertions(+), 2 deletions(-) + js/gdm/authMenuButton.js | 9 ++++----- + 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/js/gdm/authMenuButton.js b/js/gdm/authMenuButton.js -index 393dc5da4b..c5e864c686 100644 +index 40b777b5f..15d1c3785 100644 --- a/js/gdm/authMenuButton.js +++ b/js/gdm/authMenuButton.js -@@ -143,7 +143,8 @@ var AuthMenuButton = GObject.registerClass({ - }, +@@ -446,11 +446,10 @@ class AuthMenuButtonIndicator extends AuthMenuButton { + style_class: 'login-dialog-auth-menu-button-indicator-description', + y_align: Clutter.ActorAlign.CENTER, + }); +- this._descriptionLabel.bind_property_full('text', +- this._descriptionLabel, 'visible', +- GObject.BindingFlags.SYNC_CREATE, +- (bind, source) => [true, !!source], +- () => {}); ++ this._descriptionLabel.connect('notify::text', () => { ++ this._descriptionLabel.visible = !!this._descriptionLabel.text; ++ }); ++ this._descriptionLabel.visible = !!this._descriptionLabel.text; + container.add_child(this._descriptionLabel); + + this.child = container; +-- +2.55.0 + + +From 4ca92d263ba799403530d2032d0a4f69f71e5427 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Tue, 11 Aug 2026 11:48:31 +0200 +Subject: [PATCH 28/47] gdm/authMenuButton: Use 'emblem-system-symbolic' for + gear icon + +--- + 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 15d1c3785..030586399 100644 +--- a/js/gdm/authMenuButton.js ++++ b/js/gdm/authMenuButton.js +@@ -133,7 +133,7 @@ var AuthMenuButton = GObject.registerClass({ }, class AuthMenuButton extends St.Button { - _init(params) { -- params.sectionOrder ??= []; -+ if (params.sectionOrder === undefined || params.sectionOrder === null) -+ params.sectionOrder = []; + _init(params = {}) { super._init({ +- icon_name: 'cog-wheel-symbolic', ++ icon_name: 'emblem-system-symbolic', + sectionOrder: [], ...params, style_class: 'modal-dialog-button button login-dialog-auth-menu-button', -@@ -449,7 +450,8 @@ var AuthMenuButton = GObject.registerClass({ - var AuthMenuButtonIndicator = GObject.registerClass( - class AuthMenuButtonIndicator extends AuthMenuButton { - _init(params = {}) { -- params.readOnly ??= true; -+ if (params.readOnly === undefined || params.readOnly === null) -+ params.readOnly = true; - super._init(params); - - this.add_style_class_name('login-dialog-auth-menu-button-indicator'); -- -2.51.0 +2.55.0 -From f55cd19afd2028dda3af2bb1490c13ca06caf759 Mon Sep 17 00:00:00 2001 +From ccf8680c8e5b406b87fa92d22c54728195335e12 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez -Date: Fri, 12 Dec 2025 18:26:49 +0100 -Subject: [PATCH 17/30] gdm/util: Don't return a promise in +Date: Tue, 11 Aug 2026 12:36:11 +0200 +Subject: [PATCH 29/47] gdm/authPrompt: Center _authListTitle + +--- + js/gdm/authPrompt.js | 5 ++++- + 1 file changed, 4 insertions(+), 1 deletion(-) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index 70199d42d..3c9bdd06e 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -213,7 +213,10 @@ var AuthPrompt = GObject.registerClass({ + 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'}), ++ child: new St.Label({ ++ style_class: 'login-dialog-auth-list-title-label', ++ y_align: Clutter.ActorAlign.CENTER, ++ }), + visible: false, + }); + this._authList.bind_property('visible', +-- +2.55.0 + + +From 34e06cbd863249e75187842326e0b0fb56ec41c0 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Tue, 11 Aug 2026 12:53:49 +0200 +Subject: [PATCH 30/47] gdm/authPrompt: Center _authButton + +--- + js/gdm/authPrompt.js | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index 3c9bdd06e..745347a9f 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -316,6 +316,7 @@ var AuthPrompt = GObject.registerClass({ + button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE, + can_focus: true, + x_align: Clutter.ActorAlign.CENTER, ++ y_align: Clutter.ActorAlign.CENTER, + x_expand: true, + y_expand: true, + }); +-- +2.55.0 + + +From 2ded0ddbe45cd2f84cfa9d71debf29385da08857 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Tue, 11 Aug 2026 18:49:52 +0200 +Subject: [PATCH 31/47] gdm/authPrompt: Update color of spinner considering + classic theme + +--- + data/theme/gnome-shell-sass/widgets/_login-dialog.scss | 8 ++++++++ + js/gdm/authPrompt.js | 7 +++++++ + 2 files changed, 15 insertions(+) + +diff --git a/data/theme/gnome-shell-sass/widgets/_login-dialog.scss b/data/theme/gnome-shell-sass/widgets/_login-dialog.scss +index 02610878e..800455c75 100644 +--- a/data/theme/gnome-shell-sass/widgets/_login-dialog.scss ++++ b/data/theme/gnome-shell-sass/widgets/_login-dialog.scss +@@ -126,6 +126,14 @@ $_gdm_fg: $osd_fg_color; + } + } + ++.login-dialog-spinner { ++ @if $variant == 'light' { ++ color: black; ++ } @else { ++ color: $osd_fg_color; ++ } ++} ++ + .login-dialog-bottom-button-group { + padding: 32px; + spacing: 16px; +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index 745347a9f..badbd7c44 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -307,6 +307,13 @@ var AuthPrompt = GObject.registerClass({ + this._defaultButtonWell.add_child(this._nextButton); + + this._spinner = new Animation.Spinner(DEFAULT_BUTTON_WELL_ICON_SIZE); ++ this._spinner.add_style_class_name('login-dialog-spinner'); ++ this._spinnerColorEffect = new Clutter.ColorizeEffect(); ++ this._spinner.add_effect(this._spinnerColorEffect); ++ this._spinner.connect('style-changed', () => { ++ this._spinnerColorEffect.set_tint( ++ this._spinner.get_theme_node().get_foreground_color()); ++ }); + this._defaultButtonWell.add_child(this._spinner); + + this.setActorInDefaultButtonWell(this._nextButton); +-- +2.55.0 + + +From 2187952302b50e6a0b059b6798420e1b2f6ebb7a Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Tue, 11 Aug 2026 01:02:41 +0200 +Subject: [PATCH 32/47] gdm/util: Don't return a promise in wait-pending-messages The callback of wait-pending-messages was returning a Promise. @@ -2302,78 +3885,42 @@ The callback of wait-pending-messages was returning a Promise. That was causing the error "Could not guess unspecified GValue type". Avoid it embracing the callback with {} so the return isn't passed. --- - js/gdm/util.js | 2 +- + js/gdm/userVerifier.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) -diff --git a/js/gdm/util.js b/js/gdm/util.js -index 4aeec5b02a..629ba1b629 100644 ---- a/js/gdm/util.js -+++ b/js/gdm/util.js -@@ -531,7 +531,7 @@ var ShellUserVerifier = class { - authServices.connect('queue-priority-message', - (_, ...args) => this._queuePriorityMessage(...args)), - authServices.connect('wait-pending-messages', -- (_, ...args) => this._waitPendingMessages(...args)), -+ (_, ...args) => {this._waitPendingMessages(...args);}), - authServices.connect('filter-messages', - (_, ...args) => this._filterServiceMessages(...args)), - authServices.connect('verification-failed', +diff --git a/js/gdm/userVerifier.js b/js/gdm/userVerifier.js +index cba5c20cb..2977d1e0a 100644 +--- a/js/gdm/userVerifier.js ++++ b/js/gdm/userVerifier.js +@@ -547,7 +547,7 @@ var ShellUserVerifier = class { + this.emit('ask-question', args.serviceName, args.question, args.secret ?? false)), + authServices.connect('queue-message', (_, args) => this._queueMessage(args)), + authServices.connect('queue-priority-message', (_, args) => this._queuePriorityMessage(args)), +- authServices.connect('wait-pending-messages', (_, args) => this._waitPendingMessages(args)), ++ authServices.connect('wait-pending-messages', (_, args) => {this._waitPendingMessages(args);}), + authServices.connect('filter-messages', (_, args) => this._filterServiceMessages(args)), + authServices.connect('verification-failed', (_, args) => this._verificationFailed(args)), + authServices.connect('verification-complete', () => this.emit('verification-complete')), -- -2.51.0 +2.55.0 -From 179858faf126b93d734452ebb522383b07bf0a1d Mon Sep 17 00:00:00 2001 +From 7ebf22bf5cde6e283b7dbd25fd361f2f50013262 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez -Date: Fri, 12 Dec 2025 16:26:57 +0100 -Subject: [PATCH 18/30] gdm/authPrompt: Fixes +Date: Tue, 11 Aug 2026 01:04:09 +0200 +Subject: [PATCH 33/47] gdm/authPrompt: Fixes 1. Don't set directly icon_name in StButton 2. Guard setting text on entries on clear -3. Use Const instead of GdmUtil to get standard service_names -4. Inputwell was an extra container, in old version directly store in - this. -5. On setChoiceList, don't hide _message. This keeps the same layout - avoiding height increase of mainBox -6. On vfunc_key_press_event, handle event more explicitly instead of - just calling event. --- - js/gdm/authPrompt.js | 27 ++++++++++++++++++--------- - 1 file changed, 18 insertions(+), 9 deletions(-) + js/gdm/authPrompt.js | 14 +++++++++----- + 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index 6996dac18b..7f683b2ba0 100644 +index badbd7c44..907381140 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -146,8 +146,17 @@ var AuthPrompt = GObject.registerClass({ - if (keyPressEvent.keyval == Clutter.KEY_Escape) - this.cancel(); - -- if (this._preemptiveInput && !this._pendingActivate) -- return this._entry.clutter_text.event(keyPressEvent, false); -+ if (this._preemptiveInput && !this._pendingActivate) { -+ const unichar = keyPressEvent.unicode_value; -+ if (keyPressEvent.keyval === Clutter.KEY_Return && -+ this._entry.clutter_text.text) { -+ this._pendingActivate = true; -+ } else if (GLib.unichar_isprint(unichar)) { -+ this._entry.clutter_text.insert_text(unichar, -+ this._entry.clutter_text.cursor_position); -+ } -+ return Clutter.EVENT_STOP; -+ } - - return super.vfunc_key_press_event(keyPressEvent); - } -@@ -200,7 +209,7 @@ var 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 -@@ -291,7 +300,7 @@ var AuthPrompt = GObject.registerClass({ +@@ -300,7 +300,7 @@ var AuthPrompt = GObject.registerClass({ button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE, reactive: true, can_focus: false, @@ -2382,45 +3929,100 @@ index 6996dac18b..7f683b2ba0 100644 }); this._nextButton.connect('clicked', () => this._activateNext()); this._nextButton.add_style_pseudo_class('default'); -@@ -322,7 +331,7 @@ var AuthPrompt = GObject.registerClass({ - } - }); - this._webLoginDialog.connect('loading', () => this.emit('loading', this._webLoginDialog.isLoading)); -- this._inputWell.add_child(this._webLoginDialog); -+ this.add_child(this._webLoginDialog); - } - - _updateShowPasswordIcon() { -@@ -638,8 +647,10 @@ var AuthPrompt = GObject.registerClass({ +@@ -689,11 +689,15 @@ var AuthPrompt = GObject.registerClass({ }); if (!reuseEntryText) { +- this._entry.hint_text = ''; +- this._inactiveEntry.hint_text = ''; ++ if (this._entry) { ++ this._entry.hint_text = ''; ++ this._entry.text = ''; ++ } ++ if (this._inactiveEntry) { ++ this._inactiveEntry.hint_text = ''; ++ this._inactiveEntry.text = ''; ++ } + this._entryArea.hide(); - this._entry.text = ''; - this._inactiveEntry.text = ''; -+ if (this._entry) -+ this._entry.text = ''; -+ if (this._inactiveEntry) -+ this._inactiveEntry.text = ''; + this.stopSpinning(); } - this._entryArea.hide(); -@@ -712,8 +723,6 @@ var AuthPrompt = GObject.registerClass({ - } - - this._entryArea.hide(); -- if (this._message.text === '') -- this._message.hide(); - this._fadeInElement(this._authList); - } - -- -2.51.0 +2.55.0 -From bf8ecd9f1dec3a58c9734ae0b6bfb5ab6f7e0e0d Mon Sep 17 00:00:00 2001 +From 382dd6a53645e99da659f42954545c766d8f888c Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez -Date: Fri, 12 Dec 2025 18:48:48 +0100 -Subject: [PATCH 19/30] gdm/authList: Use vertical instead of orientation +Date: Tue, 11 Aug 2026 01:05:11 +0200 +Subject: [PATCH 34/47] gdm: Use St.Icon instead of iconName as property + +--- + js/gdm/authList.js | 2 +- + js/gdm/authMenuButton.js | 2 +- + 2 files changed, 2 insertions(+), 2 deletions(-) + +diff --git a/js/gdm/authList.js b/js/gdm/authList.js +index 1761922f9..44335efaf 100644 +--- a/js/gdm/authList.js ++++ b/js/gdm/authList.js +@@ -63,7 +63,7 @@ class ItemIcon extends St.Button { + super._init({ + style_class: 'login-dialog-item-icon', + button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE, +- iconName, ++ child: new St.Icon({icon_name: iconName}), + }); + + this._popup = new ItemIconPopup(this, iconTitle, iconSubtitle); +diff --git a/js/gdm/authMenuButton.js b/js/gdm/authMenuButton.js +index 030586399..c71f72a29 100644 +--- a/js/gdm/authMenuButton.js ++++ b/js/gdm/authMenuButton.js +@@ -133,7 +133,7 @@ var AuthMenuButton = GObject.registerClass({ + }, class AuthMenuButton extends St.Button { + _init(params = {}) { + super._init({ +- icon_name: 'emblem-system-symbolic', ++ child: new St.Icon({icon_name: 'emblem-system-symbolic'}), + sectionOrder: [], + ...params, + style_class: 'modal-dialog-button button login-dialog-auth-menu-button', +-- +2.55.0 + + +From 002900329c72d45cb6c062e648aae47a5ca51311 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Tue, 11 Aug 2026 12:36:29 +0200 +Subject: [PATCH 35/47] gdm/authList: Make overlay_scrollbars to make items + fill all authList width + +--- + js/gdm/authList.js | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/js/gdm/authList.js b/js/gdm/authList.js +index 44335efaf..2580ecfec 100644 +--- a/js/gdm/authList.js ++++ b/js/gdm/authList.js +@@ -182,6 +182,7 @@ var AuthList = GObject.registerClass({ + + this._scrollView = new St.ScrollView({ + style_class: 'login-dialog-auth-list-view', ++ overlay_scrollbars: true, + }); + this._scrollView.set_policy( + St.PolicyType.NEVER, St.PolicyType.AUTOMATIC); +-- +2.55.0 + + +From 43013b8f67f47c953dd87822c37a085dc7fda159 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Tue, 11 Aug 2026 01:04:45 +0200 +Subject: [PATCH 36/47] gdm/authList: Use vertical instead of orientation orientation isn't a valid property for St.BoxLayout --- @@ -2428,10 +4030,10 @@ orientation isn't a valid property for St.BoxLayout 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/js/gdm/authList.js b/js/gdm/authList.js -index 6eb34f6d97..b5072fe945 100644 +index 2580ecfec..85ff957d8 100644 --- a/js/gdm/authList.js +++ b/js/gdm/authList.js -@@ -32,7 +32,7 @@ const ItemIconPopup = class extends PopupMenu.PopupMenu { +@@ -32,7 +32,7 @@ var ItemIconPopup = class extends PopupMenu.PopupMenu { this.actor.add_style_class_name('login-dialog-item-icon-popup'); const labels = new St.BoxLayout({ @@ -2440,7 +4042,7 @@ index 6eb34f6d97..b5072fe945 100644 style_class: 'login-dialog-item-icon-popup-labels', }); labels.add_child(new St.Label({text: title})); -@@ -95,7 +95,7 @@ const AuthListItem = GObject.registerClass({ +@@ -94,7 +94,7 @@ var AuthListItem = GObject.registerClass({ x_expand: true, }); this._labelBox = new St.BoxLayout({ @@ -2449,7 +4051,7 @@ index 6eb34f6d97..b5072fe945 100644 y_align: Clutter.ActorAlign.CENTER, x_expand: true, }); -@@ -171,7 +171,7 @@ var AuthList = GObject.registerClass({ +@@ -174,7 +174,7 @@ var AuthList = GObject.registerClass({ }, class AuthList extends St.BoxLayout { _init() { super._init({ @@ -2459,67 +4061,65 @@ index 6eb34f6d97..b5072fe945 100644 y_align: Clutter.ActorAlign.CENTER, x_expand: true, -- -2.51.0 +2.55.0 -From 2c10304549a203ea1e5fca2ad6e08e0ee5b0f701 Mon Sep 17 00:00:00 2001 +From 6e8b30b09f98091fdbdaec3fed4b2f7c3b5a6d24 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez -Date: Fri, 27 Mar 2026 17:47:17 +0100 -Subject: [PATCH 20/30] gdm/authList: Use St.Icon instead of iconName as - property +Date: Tue, 11 Aug 2026 11:34:18 +0200 +Subject: [PATCH 37/47] gdm/authList: Refactor 'child-added'/'child-removed' +BoxLayout doesn't expose those signals in this old version. +Just call _updateItemsLayout() on addItem(), removeItem() and clear(), +which results the same. --- - js/gdm/authList.js | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) + js/gdm/authList.js | 5 +++-- + 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/js/gdm/authList.js b/js/gdm/authList.js -index b5072fe945..0c8ef1f18d 100644 +index 85ff957d8..96a59f981 100644 --- a/js/gdm/authList.js +++ b/js/gdm/authList.js -@@ -62,7 +62,7 @@ class ItemIcon extends St.Button { - _init(iconName, iconTitle, iconSubtitle) { - super._init({ - style_class: 'login-dialog-item-icon', -- iconName, -+ child: new St.Icon({icon_name: iconName}), - }); +@@ -197,8 +197,6 @@ var AuthList = GObject.registerClass({ + this._scrollView.add_actor(this._box); + this._items = new Map(); - this._popup = new ItemIconPopup(this, iconTitle, iconSubtitle); +- this._box.connect('child-added', () => this._updateItemsLayout()); +- this._box.connect('child-removed', () => this._updateItemsLayout()); + this.connect('key-focus-in', this._moveFocusToItems.bind(this)); + } + +@@ -260,6 +258,7 @@ var AuthList = GObject.registerClass({ + item.connect('key-focus-in', () => this.scrollToItem(item)); + + this._moveFocusToItems(); ++ this._updateItemsLayout(); + + this.emit('item-added', item); + } +@@ -273,6 +272,7 @@ var AuthList = GObject.registerClass({ + item.destroy(); + + this._items.delete(key); ++ this._updateItemsLayout(); + } + + get numItems() { +@@ -282,5 +282,6 @@ var AuthList = GObject.registerClass({ + clear() { + this._box.destroy_all_children(); + this._items.clear(); ++ this._updateItemsLayout(); + } + }); -- -2.51.0 +2.55.0 -From 480c3f08428161c3f5ef2e85d7ddc7e9a0c42737 Mon Sep 17 00:00:00 2001 +From 8dbf21fa2792710548df29574b9b00e96185cecd Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez -Date: Fri, 12 Dec 2025 19:02:04 +0100 -Subject: [PATCH 21/30] gdm/loginDialog: Use propper icon name for - authMenuButton - ---- - js/gdm/loginDialog.js | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) - -diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js -index f59663401b..ab5b9c3d77 100644 ---- a/js/gdm/loginDialog.js -+++ b/js/gdm/loginDialog.js -@@ -445,7 +445,7 @@ var LoginDialog = GObject.registerClass({ - _createAuthMenuButton() { - this._authMenuButton = new AuthMenuButton.AuthMenuButton({ - title: _('Login Options'), -- iconName: 'cog-wheel-symbolic', -+ iconName: 'emblem-system-symbolic', - sectionOrder: [_PRIMARY_LOGIN_METHOD_SECTION_NAME, _SESSION_TYPE_SECTION_NAME], - }); - this._authMenuButton.updateSensitivity(false); --- -2.51.0 - - -From 56de4883ea483307607d148e2880da220cf782c5 Mon Sep 17 00:00:00 2001 -From: Joan Torres Lopez -Date: Wed, 1 Apr 2026 13:05:30 +0200 -Subject: [PATCH 22/30] gdm/LoginDialog: Reduce min height of authprompt +Date: Tue, 11 Aug 2026 01:05:41 +0200 +Subject: [PATCH 38/47] gdm/LoginDialog: Reduce min height of authprompt authprompt is smaller so min height has to be smaller --- @@ -2528,52 +4128,108 @@ authprompt is smaller so min height has to be smaller 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js -index ab5b9c3d77..cdf6f23e2d 100644 +index efd33581c..bdb91f138 100644 --- a/js/gdm/loginDialog.js +++ b/js/gdm/loginDialog.js -@@ -34,7 +34,7 @@ const UserWidget = imports.ui.userWidget; +@@ -37,7 +37,7 @@ const {MessageType} = imports.gdm.userVerifier; const _FADE_ANIMATION_TIME = 250; const _SCROLL_ANIMATION_TIME = 500; --const _FIXED_TOP_ACTOR_HEIGHT = 400; +-const _FIXED_TOP_ACTOR_HEIGHT = 550; +const _FIXED_TOP_ACTOR_HEIGHT = 300; const _TIMED_LOGIN_IDLE_THRESHOLD = 5.0; const _PRIMARY_LOGIN_METHOD_SECTION_NAME = _('Login Options'); const _SESSION_TYPE_SECTION_NAME = _('Session Type'); diff --git a/js/ui/unlockDialog.js b/js/ui/unlockDialog.js -index 654959190e..477edbe8fd 100644 +index 3df9b0e64..00b0de2a4 100644 --- a/js/ui/unlockDialog.js +++ b/js/ui/unlockDialog.js -@@ -32,7 +32,7 @@ const BLUR_SIGMA = 60; +@@ -28,7 +28,7 @@ const FADE_OUT_SCALE = 0.3; + const BLUR_BRIGHTNESS = 0.55; + const BLUR_SIGMA = 60; + +-const FIXED_PROMPT_HEIGHT = 550; ++const FIXED_PROMPT_HEIGHT = 300; const SUMMARY_ICON_SIZE = 32; --const FIXED_PROMPT_HEIGHT = 400; -+const FIXED_PROMPT_HEIGHT = 300; - - var NotificationsBox = GObject.registerClass({ - Signals: { 'wake-up-screen': {} }, -- -2.51.0 +2.55.0 -From 074089190aad31dc52f4da9748a2bda7cb04f239 Mon Sep 17 00:00:00 2001 +From 88d44c069907eb2ea52b38fbd6ad59bcfcaad614 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez -Date: Fri, 12 Dec 2025 16:21:02 +0100 -Subject: [PATCH 23/30] gdm/webLogin: Some fixes for backport +Date: Tue, 11 Aug 2026 13:22:16 +0200 +Subject: [PATCH 39/47] gdm: Vertically center webLoginDialog without fixed top + +This old version was positioning it in a down-offset. +--- + js/gdm/loginDialog.js | 5 ++++- + js/ui/unlockDialog.js | 15 ++++++++++++--- + 2 files changed, 16 insertions(+), 4 deletions(-) + +diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js +index bdb91f138..3b0661505 100644 +--- a/js/gdm/loginDialog.js ++++ b/js/gdm/loginDialog.js +@@ -618,7 +618,10 @@ var LoginDialog = GObject.registerClass({ + let authPromptAllocation = null; + let authPromptWidth = 0; + if (this._authPrompt.visible) { +- authPromptAllocation = this._getFixedTopActorAllocation(dialogBox, this._authPrompt); ++ if (this._authPrompt.webLoginActive) ++ authPromptAllocation = this._getCenterActorAllocation(dialogBox, this._authPrompt); ++ else ++ authPromptAllocation = this._getFixedTopActorAllocation(dialogBox, this._authPrompt); + authPromptWidth = authPromptAllocation.x2 - authPromptAllocation.x1; + } + +diff --git a/js/ui/unlockDialog.js b/js/ui/unlockDialog.js +index 00b0de2a4..01ff03b2c 100644 +--- a/js/ui/unlockDialog.js ++++ b/js/ui/unlockDialog.js +@@ -462,9 +462,18 @@ class UnlockDialogLayout extends Clutter.LayoutManager { + this._notifications.allocate(actorBox); + + // Authentication Box +- const stackY = Math.min( +- Math.floor(centerY - FIXED_PROMPT_HEIGHT / 2.0), +- height - stackHeight - maxNotificationsHeight); ++ const dialog = container.get_parent(); ++ let stackY; ++ if (dialog._activePage === dialog._clock || ++ dialog._authPrompt?.webLoginActive) { ++ stackY = Math.min( ++ Math.floor(centerY - stackHeight / 2.0), ++ height - stackHeight - maxNotificationsHeight); ++ } else { ++ stackY = Math.min( ++ Math.floor(centerY - FIXED_PROMPT_HEIGHT / 2.0), ++ height - stackHeight - maxNotificationsHeight); ++ } + + actorBox.x1 = columnX1; + actorBox.y1 = stackY; +-- +2.55.0 + + +From 8c506a989c60e671e9311d23429a793539f763e6 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Tue, 11 Aug 2026 01:07:15 +0200 +Subject: [PATCH 40/47] gdm/webLogin: Some fixes for backport 1. Use connect with notify::reactive because bind_property_full is not available 2. Use vertical property because orientation isn't available -3. Set '' as the default value for message --- - js/gdm/webLogin.js | 15 +++++++-------- - 1 file changed, 7 insertions(+), 8 deletions(-) + js/gdm/webLogin.js | 13 ++++++------- + 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/js/gdm/webLogin.js b/js/gdm/webLogin.js -index f4972144fa..d2e947f646 100644 +index 9304332ac..d7b051d9f 100644 --- a/js/gdm/webLogin.js +++ b/js/gdm/webLogin.js -@@ -144,14 +144,13 @@ var WebLoginDialog = GObject.registerClass({ +@@ -140,14 +140,13 @@ var WebLoginDialog = GObject.registerClass({ y_expand: true, visible: false, }); @@ -2593,7 +4249,7 @@ index f4972144fa..d2e947f646 100644 x_expand: true, y_expand: true, }); -@@ -161,7 +160,7 @@ var WebLoginDialog = GObject.registerClass({ +@@ -157,7 +156,7 @@ var WebLoginDialog = GObject.registerClass({ this._contentBox.add_child(this._webLoginPrompt); this._buttonBox = new St.BoxLayout({ @@ -2602,64 +4258,39 @@ index f4972144fa..d2e947f646 100644 x_align: Clutter.ActorAlign.CENTER, x_expand: true, }); -@@ -290,7 +289,7 @@ var WebLoginIntro = GObject.registerClass( - class WebLoginIntro extends St.Button { - _init(params) { - const {message} = Params.parse(params, { -- message: null, -+ message: '', - }); - - const label = new St.Label({ -- -2.51.0 +2.55.0 -From 908cce536d7933bb09138e4022af46813fe82092 Mon Sep 17 00:00:00 2001 +From 632e4c73e57e0469105688b256fd4b134f0ee957 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez -Date: Mon, 15 Dec 2025 13:36:31 +0100 -Subject: [PATCH 24/30] gdm/webLogin: Vertically center align labels of buttons +Date: Tue, 11 Aug 2026 01:07:44 +0200 +Subject: [PATCH 41/47] gdm/webLogin: Vertically center align labels of buttons --- - js/gdm/webLogin.js | 3 +++ - 1 file changed, 3 insertions(+) + js/gdm/webLogin.js | 1 + + 1 file changed, 1 insertion(+) diff --git a/js/gdm/webLogin.js b/js/gdm/webLogin.js -index d2e947f646..062ad5f309 100644 +index d7b051d9f..925669f17 100644 --- a/js/gdm/webLogin.js +++ b/js/gdm/webLogin.js -@@ -205,6 +205,7 @@ var WebLoginDialog = GObject.registerClass({ +@@ -202,6 +202,7 @@ var WebLoginDialog = GObject.registerClass({ child: new St.Label({ text: b.label, - style_class: 'web-login-button-label', + style_class: 'login-button-label', + y_align: Clutter.ActorAlign.CENTER, }), }); -@@ -295,6 +296,7 @@ class WebLoginIntro extends St.Button { - const label = new St.Label({ - text: message, - style_class: 'web-login-button-label', -+ y_align: Clutter.ActorAlign.CENTER, - }); - - super._init({ -@@ -304,6 +306,7 @@ class WebLoginIntro extends St.Button { - reactive: true, - can_focus: true, - child: label, -+ y_align: Clutter.ActorAlign.CENTER, - }); - } - -- -2.51.0 +2.55.0 -From 0e08e9a99057f24e3f8e57d66115d8368d00f2d0 Mon Sep 17 00:00:00 2001 +From bcefd5e1956628e2197a37e937f89665bab78d61 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez -Date: Mon, 15 Dec 2025 13:53:08 +0100 -Subject: [PATCH 25/30] gdm/webLogin: Set data to qr texture with the right +Date: Tue, 11 Aug 2026 01:08:14 +0200 +Subject: [PATCH 42/47] gdm/webLogin: Set data to qr texture with the right args This old version has a different signature for setting text data. @@ -2668,7 +4299,7 @@ This old version has a different signature for setting text data. 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/js/ui/qrCode.js b/js/ui/qrCode.js -index 547ad7d153..adf6e40051 100644 +index bc73d337d..5f6ca8335 100644 --- a/js/ui/qrCode.js +++ b/js/ui/qrCode.js @@ -87,14 +87,12 @@ var QrCode = GObject.registerClass({ @@ -2683,19 +4314,19 @@ index 547ad7d153..adf6e40051 100644 - content.set_bytes( - coglContext, - pixelData, -+ content.set_data( -+ pixelData.get_data(), ++ content.set_data( ++ pixelData.get_data(), Cogl.PixelFormat.RGBA_8888, qrSize, qrSize, -- -2.51.0 +2.55.0 -From 97ac6d29ff531f15dcadd907164ae5c4609bb016 Mon Sep 17 00:00:00 2001 +From dd7c87c9d51079c19164c4f4c61abf6f1e8fd3f1 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez -Date: Fri, 26 Dec 2025 12:30:43 +0100 -Subject: [PATCH 26/30] gdm/webLogin: Make spinner size 16 +Date: Tue, 11 Aug 2026 01:08:26 +0200 +Subject: [PATCH 43/47] gdm/webLogin: Make spinner size 16 In old versions spinner icon would be duplicated to fill the space instead of scaled. @@ -2704,11 +4335,11 @@ instead of scaled. 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/gdm/webLogin.js b/js/gdm/webLogin.js -index 062ad5f309..ac79816699 100644 +index 925669f17..ef220101c 100644 --- a/js/gdm/webLogin.js +++ b/js/gdm/webLogin.js -@@ -12,7 +12,7 @@ const { QrCode } = imports.ui.qrCode; - Gio._promisify(GnomeQR, 'generate_qr_code_async', 'generate_qr_code_finish'); +@@ -8,7 +8,7 @@ const Params = imports.misc.params; + const { QrCode } = imports.ui.qrCode; const QR_CODE_SIZE = 150; -const WEB_LOGIN_SPINNER_SIZE = 35; @@ -2717,111 +4348,25 @@ index 062ad5f309..ac79816699 100644 var WebLoginPrompt = GObject.registerClass( -- -2.51.0 +2.55.0 -From 36e72660620d28929d250ee958df0eb0834a163d Mon Sep 17 00:00:00 2001 +From 13816c897a26a0e44439ccc986a8db696c0520c7 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez -Date: Fri, 26 Dec 2025 12:31:48 +0100 -Subject: [PATCH 27/30] gdm/unlockDialog: Use proper icon for gear - -In old versions the icon name is called 'emblem-system-symbolic' ---- - js/ui/unlockDialog.js | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) - -diff --git a/js/ui/unlockDialog.js b/js/ui/unlockDialog.js -index 477edbe8fd..85b512aa85 100644 ---- a/js/ui/unlockDialog.js -+++ b/js/ui/unlockDialog.js -@@ -646,7 +646,7 @@ var UnlockDialog = GObject.registerClass({ - // Login Options button - this._authMenuButton = new AuthMenuButton.AuthMenuButton({ - title: _('Login Options'), -- iconName: 'cog-wheel-symbolic', -+ iconName: 'emblem-system-symbolic', - }); - this._authMenuButton.connect('active-item-changed', () => { - const authMechanism = this._authMenuButton.getActiveItem(); --- -2.51.0 - - -From 39d93c9d86f99e3ed46544d052ceae188e2adc25 Mon Sep 17 00:00:00 2001 -From: Joan Torres Lopez -Date: Fri, 27 Mar 2026 17:53:33 +0100 -Subject: [PATCH 28/30] gdm/unlockDialog: Use vertical boolean instead - orientation - ---- - js/ui/unlockDialog.js | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) - -diff --git a/js/ui/unlockDialog.js b/js/ui/unlockDialog.js -index 85b512aa85..ab4141e2c3 100644 ---- a/js/ui/unlockDialog.js -+++ b/js/ui/unlockDialog.js -@@ -335,7 +335,7 @@ class UnlockDialogClock extends St.BoxLayout { - _init() { - super._init({ - style_class: 'unlock-dialog-clock', -- orientation: Clutter.Orientation.VERTICAL, -+ vertical: true, - y_align: Clutter.ActorAlign.CENTER, - }); - --- -2.51.0 - - -From 0b4f7dcb4ec6821e38888898a8298a1e8a5e88bf Mon Sep 17 00:00:00 2001 -From: Joan Torres Lopez -Date: Thu, 2 Apr 2026 18:55:31 +0200 -Subject: [PATCH 29/30] gdm/unlockDialog: Don't use timeout_add_once - ---- - js/gdm/authServicesLegacy.js | 3 ++- - 1 file changed, 2 insertions(+), 1 deletion(-) - -diff --git a/js/gdm/authServicesLegacy.js b/js/gdm/authServicesLegacy.js -index 5955aeadff..4c5a49effa 100644 ---- a/js/gdm/authServicesLegacy.js -+++ b/js/gdm/authServicesLegacy.js -@@ -136,12 +136,13 @@ var AuthServicesLegacy = GObject.registerClass({ - 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; - }); - } - --- -2.51.0 - - -From 17a21d8e27fddb369977c50e9e62de177222120b Mon Sep 17 00:00:00 2001 -From: Joan Torres Lopez -Date: Fri, 26 Dec 2025 12:34:11 +0100 -Subject: [PATCH 30/30] fingerprintManager: Update getDefaultService using +Date: Tue, 11 Aug 2026 01:09:19 +0200 +Subject: [PATCH 44/47] fingerprintManager: Update getDefaultService using remote This is how async methods are called in this version. --- - js/misc/fingerprintManager.js | 32 ++++++++++++++++---------------- + js/gdm/fingerprintManager.js | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) -diff --git a/js/misc/fingerprintManager.js b/js/misc/fingerprintManager.js -index 151e787e0b..4467e0bfa9 100644 ---- a/js/misc/fingerprintManager.js -+++ b/js/misc/fingerprintManager.js -@@ -64,23 +64,23 @@ var FingerprintManager = GObject.registerClass({ +diff --git a/js/gdm/fingerprintManager.js b/js/gdm/fingerprintManager.js +index d429001c5..5bc7b55af 100644 +--- a/js/gdm/fingerprintManager.js ++++ b/js/gdm/fingerprintManager.js +@@ -65,23 +65,23 @@ var FingerprintManager = GObject.registerClass({ } async checkReaderType(cancellable = null) { @@ -2862,5 +4407,180 @@ index 151e787e0b..4467e0bfa9 100644 async _initFingerprintManagerProxy() { -- -2.51.0 +2.55.0 + + +From 18948242ad3db2a719ec883c492239d9916e04f5 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Tue, 11 Aug 2026 01:18:01 +0200 +Subject: [PATCH 45/47] gdm: Use property name as nick/blurb in ParamSpec + +Passing null for nick/blurb to auto-derive them from the property name +isn't supported in this old version. Every ParamSpec in this codebase +uses the property name for nick and blurb. +--- + js/gdm/authMenuButton.js | 6 +++--- + js/gdm/fingerprintManager.js | 2 +- + js/ui/qrCode.js | 2 +- + 3 files changed, 5 insertions(+), 5 deletions(-) + +diff --git a/js/gdm/authMenuButton.js b/js/gdm/authMenuButton.js +index c71f72a29..96b1a95b1 100644 +--- a/js/gdm/authMenuButton.js ++++ b/js/gdm/authMenuButton.js +@@ -116,14 +116,14 @@ class AuthMenuItem extends PopupMenu.PopupBaseMenuItem { + var AuthMenuButton = GObject.registerClass({ + Properties: { + 'read-only': GObject.ParamSpec.boolean( +- 'read-only', null, null, ++ 'read-only', 'read-only', 'read-only', + GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY, + false), + 'section-order': GObject.ParamSpec.jsobject( +- 'section-order', null, null, ++ 'section-order', 'section-order', 'section-order', + GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY), + 'animate-visibility': GObject.ParamSpec.boolean( +- 'animate-visibility', null, null, ++ 'animate-visibility', 'animate-visibility', 'animate-visibility', + GObject.ParamFlags.READWRITE, + false), + }, +diff --git a/js/gdm/fingerprintManager.js b/js/gdm/fingerprintManager.js +index 5bc7b55af..5cf876cb1 100644 +--- a/js/gdm/fingerprintManager.js ++++ b/js/gdm/fingerprintManager.js +@@ -30,7 +30,7 @@ function getFingerprintManager() { + var FingerprintManager = GObject.registerClass({ + Properties: { + 'reader-type': GObject.ParamSpec.uint( +- 'reader-type', null, null, ++ 'reader-type', 'reader-type', 'reader-type', + GObject.ParamFlags.READWRITE, + FingerprintReaderType.NONE, FingerprintReaderType.SWIPE, + FingerprintReaderType.NONE), +diff --git a/js/ui/qrCode.js b/js/ui/qrCode.js +index 5f6ca8335..d76999a1e 100644 +--- a/js/ui/qrCode.js ++++ b/js/ui/qrCode.js +@@ -10,7 +10,7 @@ const QR_CODE_TRANSPARENT_COLOR = new GnomeQR.Color({alpha: 0}); + var QrCode = GObject.registerClass({ + Properties: { + 'url': GObject.ParamSpec.string( +- 'url', null, null, ++ 'url', 'url', 'url', + GObject.ParamFlags.READWRITE, + null), + }, +-- +2.55.0 + + +From 7bc081fc93915d4676c814b6b4b0c4096e1ac44e Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Wed, 12 Aug 2026 12:42:29 +0200 +Subject: [PATCH 46/47] gdm: Add backward-compatible property and method shims + +Several ShellUserVerifier properties and methods were dropped when +the class gained support for multiple concurrently-active AuthServices +(reauthenticating, smartcardDetected, serviceIsForeground(), +foregroundServiceDeterminesUsername(), serviceIsDefault(), +serviceIsFingerprint(), finishMessageQueue()), and AuthPrompt's +addCharacter() was renamed to startPreemptiveInput(). + +Restore reauthenticating as a real, tracked property, and add the +rest back as deprecated shims (documented with why they can no longer +be meaningfully implemented) so extensions calling them don't crash +with a TypeError. +--- + js/gdm/userVerifier.js | 45 ++++++++++++++++++++++++++++++++++++++++++ + 1 file changed, 45 insertions(+) + +diff --git a/js/gdm/userVerifier.js b/js/gdm/userVerifier.js +index 2977d1e0a..020cb4f7b 100644 +--- a/js/gdm/userVerifier.js ++++ b/js/gdm/userVerifier.js +@@ -246,6 +246,51 @@ var ShellUserVerifier = class { + this._finishMessageQueue(); + } + ++ /** ++ * @deprecated Kept for backward compatibility. Replaced by needsUsername(), ++ * of which this is the logical inverse: the "foreground service" concept ++ * no longer exists now that multiple AuthServices can be active at once, ++ * each exposing its own selectable mechanisms. ++ */ ++ foregroundServiceDeterminesUsername() { ++ return !this.needsUsername(); ++ } ++ ++ /** ++ * @deprecated Kept for backward compatibility, always returns false. ++ * There is no single "foreground service" anymore: each AuthServices ++ * instance tracks its own selected mechanism (see selectedMechanism). ++ */ ++ serviceIsForeground(_serviceName) { ++ return false; ++ } ++ ++ /** ++ * @deprecated Kept for backward compatibility, always returns false. ++ * Service roles are now assigned dynamically across AuthServices ++ * instances (see _redistributeRoles()) rather than through a single ++ * fixed default service. ++ */ ++ serviceIsDefault(_serviceName) { ++ return false; ++ } ++ ++ /** ++ * @deprecated Kept for backward compatibility, always returns false. ++ * Fingerprint handling is now internal to AuthServicesLegacy; use the ++ * 'mechanisms-changed' signal and check a mechanism's role instead. ++ */ ++ serviceIsFingerprint(_serviceName) { ++ return false; ++ } ++ ++ /** ++ * @deprecated Renamed to the private _finishMessageQueue(). ++ */ ++ finishMessageQueue() { ++ this._finishMessageQueue(); ++ } ++ + setDriverService(serviceName) { + if (this._driverService === serviceName) + return; +-- +2.55.0 + + +From b040df8d6ef6a353a513d9617b760afe989de9ff Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Thu, 13 Aug 2026 14:05:58 +0200 +Subject: [PATCH 47/47] gdm/userVerifier: Fix use of error.cause + +This old version of gjs didn't support .cause property in error. +Explicitly set it to fix it. +--- + js/gdm/userVerifier.js | 3 ++- + 1 file changed, 2 insertions(+), 1 deletion(-) + +diff --git a/js/gdm/userVerifier.js b/js/gdm/userVerifier.js +index 020cb4f7b..8e91d1060 100644 +--- a/js/gdm/userVerifier.js ++++ b/js/gdm/userVerifier.js +@@ -105,7 +105,8 @@ const AuthServicesClasses = [ + */ + var InitError = class InitError extends Error { + constructor(error, message, serviceName) { +- super(message, {cause: error}); ++ super(message); ++ this.cause = error; + this.serviceName = serviceName; + } + } +-- +2.55.0 diff --git a/pre-changes-for-passwordless-gdm-backport.patch b/pre-changes-for-passwordless-gdm-backport.patch index 3a9b58e..0cc6bda 100644 --- a/pre-changes-for-passwordless-gdm-backport.patch +++ b/pre-changes-for-passwordless-gdm-backport.patch @@ -1,7 +1,7 @@ -From 38531053c836ca7ae91e5f20785a67e690422d01 Mon Sep 17 00:00:00 2001 +From 707869667dc400850b5bc21bf27bb486e9ed9048 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= Date: Mon, 25 Aug 2025 14:25:49 +0200 -Subject: [PATCH 1/6] gdm/authPrompt: Unset the entry reference on destruction +Subject: [PATCH 1/7] gdm/authPrompt: Unset the entry reference on destruction We use the internal entry as the reference to the text entry that is currently in use, but during destruction we do not unset it, thus the @@ -15,7 +15,7 @@ Part-of: 1 file changed, 1 insertion(+) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index 2b65eee8b9..e4950e56ce 100644 +index 2b65eee8b..e4950e56c 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js @@ -133,6 +133,7 @@ var AuthPrompt = GObject.registerClass({ @@ -27,13 +27,13 @@ index 2b65eee8b9..e4950e56ce 100644 vfunc_key_press_event(keyPressEvent) { -- -2.52.0 +2.55.0 -From 34ff1bdba494f66282cba3c8a955b48b041371cc Mon Sep 17 00:00:00 2001 +From 0704d6d04d920fd65ff4be8b8cc5583de69a9219 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= Date: Mon, 25 Aug 2025 14:29:55 +0200 -Subject: [PATCH 2/6] gdm/authPrompt: Clear the inactive entry too +Subject: [PATCH 2/7] gdm/authPrompt: Clear the inactive entry too In multi-factor authentication cases we may end up clearing the current entry, but we are potentially leaving a previously filled inactive entry @@ -47,7 +47,7 @@ Part-of: 1 file changed, 1 insertion(+) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index e4950e56ce..2d306d16b5 100644 +index e4950e56c..2d306d16b 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js @@ -489,6 +489,7 @@ var AuthPrompt = GObject.registerClass({ @@ -59,13 +59,13 @@ index e4950e56ce..2d306d16b5 100644 this._authList.clear(); this._authList.hide(); -- -2.52.0 +2.55.0 -From 576c74f85fe316ef541a347bd51134ceb091658a Mon Sep 17 00:00:00 2001 +From 71933c370e984a48e3f6da45c6b5cf9a345097c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= Date: Tue, 26 Aug 2025 16:40:07 +0200 -Subject: [PATCH 3/6] gdm/authPrompt: Factorize the entry update code +Subject: [PATCH 3/7] gdm/authPrompt: Factorize the entry update code Cleanup the code a bit to make future updates smaller @@ -75,7 +75,7 @@ Part-of: 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index 2d306d16b5..ed42409b12 100644 +index 2d306d16b..ed42409b1 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js @@ -305,13 +305,22 @@ var AuthPrompt = GObject.registerClass({ @@ -106,13 +106,13 @@ index 2d306d16b5..ed42409b12 100644 } -- -2.52.0 +2.55.0 -From 2b9dd08c26da92ed0b2e86356fb26a1b69d02f66 Mon Sep 17 00:00:00 2001 +From 980a1972b71fcd2ab0435da88082f2421d1d46e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= Date: Mon, 25 Aug 2025 14:35:57 +0200 -Subject: [PATCH 4/6] gdm/authPrompt: Preserve the text when switching entry +Subject: [PATCH 4/7] gdm/authPrompt: Preserve the text when switching entry visibility The text visibility of an auth prompt entry may change dynamically at @@ -144,7 +144,7 @@ Part-of: 1 file changed, 3 insertions(+) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index ed42409b12..e4084ad22d 100644 +index ed42409b1..e4084ad22 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js @@ -319,6 +319,9 @@ var AuthPrompt = GObject.registerClass({ @@ -158,13 +158,13 @@ index ed42409b12..e4084ad22d 100644 this._capsLockWarningLabel.visible = secret; -- -2.52.0 +2.55.0 -From 1670d0e80215cd3c934dc1de79997753b9eafdf8 Mon Sep 17 00:00:00 2001 +From ee671464acb9c5b86157c4d9730533379ee09843 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= Date: Fri, 22 Aug 2025 15:08:23 +0200 -Subject: [PATCH 5/6] gdm/authPrompt: Fix key focus handling on choice list +Subject: [PATCH 5/7] gdm/authPrompt: Fix key focus handling on choice list When a choice list widget is prompted in GDM we call updateSensitivity() but this does not act on the currently visible authentication widget but @@ -186,7 +186,7 @@ Part-of: 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index e4084ad22d..f9205d41dd 100644 +index e4084ad22..f9205d41d 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js @@ -524,13 +524,13 @@ var AuthPrompt = GObject.registerClass({ @@ -237,13 +237,13 @@ index e4084ad22d..f9205d41dd 100644 } -- -2.52.0 +2.55.0 -From c82bbfa5eeb3e3e6455f9d168b0463183d8eaf0a Mon Sep 17 00:00:00 2001 +From 568eddde54ef020166a9c553cc88655d1aff1568 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCllner?= Date: Tue, 8 Jul 2025 17:42:56 +0200 -Subject: [PATCH 6/6] loginDialog: Move session/a11y buttons into a box +Subject: [PATCH 6/7] loginDialog: Move session/a11y buttons into a box The intermediate container allows the theme to control the distance from the edge and spacing between buttons, which is more accessible @@ -256,7 +256,7 @@ Part-of: 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/data/theme/gnome-shell-sass/widgets/_login-dialog.scss b/data/theme/gnome-shell-sass/widgets/_login-dialog.scss -index f68d5de996..307a751628 100644 +index f68d5de99..307a75162 100644 --- a/data/theme/gnome-shell-sass/widgets/_login-dialog.scss +++ b/data/theme/gnome-shell-sass/widgets/_login-dialog.scss @@ -89,6 +89,11 @@ @@ -272,10 +272,10 @@ index f68d5de996..307a751628 100644 .login-dialog-banner { color: darken($osd_fg_color,10%); } .login-dialog-button-box { width: 23em; spacing: 5px; } diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js -index fbced3e4ab..a218e59d78 100644 +index 36ecfe444..8ed0a0d32 100644 --- a/js/gdm/loginDialog.js +++ b/js/gdm/loginDialog.js -@@ -559,6 +559,11 @@ var LoginDialog = GObject.registerClass({ +@@ -493,6 +493,11 @@ var LoginDialog = GObject.registerClass({ bannerBox.add_child(this._bannerLabel); this._updateBanner(); @@ -287,7 +287,7 @@ index fbced3e4ab..a218e59d78 100644 this._sessionMenuButton = new SessionMenuButton(); this._sessionMenuButton.connect('session-activated', (list, sessionId) => { -@@ -566,7 +571,7 @@ var LoginDialog = GObject.registerClass({ +@@ -500,7 +505,7 @@ var LoginDialog = GObject.registerClass({ }); this._sessionMenuButton.opacity = 0; this._sessionMenuButton.show(); @@ -296,7 +296,7 @@ index fbced3e4ab..a218e59d78 100644 this._logoBin = new St.Widget({ style_class: 'login-dialog-logo-bin', x_align: Clutter.ActorAlign.CENTER, -@@ -625,17 +630,17 @@ var LoginDialog = GObject.registerClass({ +@@ -559,17 +564,17 @@ var LoginDialog = GObject.registerClass({ return actorBox; } @@ -319,7 +319,7 @@ index fbced3e4ab..a218e59d78 100644 actorBox.x2 = actorBox.x1 + natWidth; actorBox.y2 = actorBox.y1 + natHeight; -@@ -698,9 +703,9 @@ var LoginDialog = GObject.registerClass({ +@@ -632,9 +637,9 @@ var LoginDialog = GObject.registerClass({ logoHeight = logoAllocation.y2 - logoAllocation.y1; } @@ -332,7 +332,7 @@ index fbced3e4ab..a218e59d78 100644 // Then figure out if we're overly constrained and need to // try a different layout, or if we have what extra space we -@@ -801,8 +806,8 @@ var LoginDialog = GObject.registerClass({ +@@ -735,8 +740,8 @@ var LoginDialog = GObject.registerClass({ if (logoAllocation) this._logoBin.allocate(logoAllocation); @@ -344,5 +344,91 @@ index fbced3e4ab..a218e59d78 100644 _ensureUserListLoaded() { -- -2.52.0 +2.55.0 + + +From 9110b24bf3d48ed73077c8df21289ab38bad55f0 Mon Sep 17 00:00:00 2001 +From: Alessandro Bono +Date: Fri, 5 Aug 2022 12:56:04 +0200 +Subject: [PATCH 7/7] authPrompt: Don't propagate serviceName to setMessage + +There isn't always a serviceName when we set a message. Furthermore we +are passing the serviceName only to decide if we want to wiggle or not. +Pass the wiggle parameters instead. If they are missing, we don't want +to wiggle. + +This fixes the following error: +JS ERROR: Exception in callback for signal: login-format-changed: Error: Wrong type number; string expected +setMessage@resource:///org/gnome/shell/gdm/authPrompt.js:542:13 +_showRealmLoginHint@resource:///org/gnome/shell/gdm/loginDialog.js:931:26 +_emit@resource:///org/gnome/gjs/modules/core/_signals.js:114:47 +_updateLoginFormat@resource:///org/gnome/shell/gdm/realmd.js:85:18 +_reloadRealm@resource:///org/gnome/shell/gdm/realmd.js:57:14 +_onRealmLoaded@resource:///org/gnome/shell/gdm/realmd.js:64:14 +_makeProxyWrapper/ +--- + js/gdm/authPrompt.js | 29 ++++++++++++++++------------- + 1 file changed, 16 insertions(+), 13 deletions(-) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index f9205d41d..11f34fc2c 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -392,7 +392,20 @@ var AuthPrompt = GObject.registerClass({ + } + + _onShowMessage(_userVerifier, serviceName, message, type) { +- this.setMessage(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); + this.emit('prompted'); + } + +@@ -572,7 +585,7 @@ var AuthPrompt = GObject.registerClass({ + }); + } + +- setMessage(serviceName, message, type) { ++ setMessage(message, type, wiggleParameters = {duration: 0}) { + if (type == GdmUtil.MessageType.ERROR) + this._message.add_style_class_name('login-dialog-message-warning'); + else +@@ -592,17 +605,7 @@ var AuthPrompt = GObject.registerClass({ + this._message.opacity = 0; + } + +- 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)); +- Util.wiggle(this._message, wiggleParameters); +- } ++ Util.wiggle(this._message, wiggleParameters); + } + + updateSensitivity(sensitive) { +-- +2.55.0