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 46fcc5a..4ff3a6c 100644 --- a/0001-Support-for-web-login-and-unified-auth-mechanism.patch +++ b/0001-Support-for-web-login-and-unified-auth-mechanism.patch @@ -1,7 +1,7 @@ -From 3c1861b42dfcefafa240bae0270702c7d9c9a2fd Mon Sep 17 00:00:00 2001 +From 27673e15fbde961e7f42a8517d2a5c4a6452843e Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Thu, 2 Oct 2025 10:59:57 +0200 -Subject: [PATCH 01/42] style: Add common login dialog button styles to avoid +Subject: [PATCH 01/54] style: Add common login dialog button styles to avoid duplication This will be used in next commits, when new login buttons are added. @@ -15,7 +15,7 @@ without it the button was being dark when insensitive. 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/data/theme/gnome-shell-sass/_common.scss b/data/theme/gnome-shell-sass/_common.scss -index 846427e8e0..1f14286f9f 100644 +index 846427e8e..1f14286f9 100644 --- a/data/theme/gnome-shell-sass/_common.scss +++ b/data/theme/gnome-shell-sass/_common.scss @@ -378,3 +378,14 @@ stage { @@ -34,7 +34,7 @@ index 846427e8e0..1f14286f9f 100644 + &:insensitive { @include button(insensitive, $tc:$system_fg_color, $c:$system_base_color, $style: $style, $always_dark: true);} +} diff --git a/data/theme/gnome-shell-sass/_drawing.scss b/data/theme/gnome-shell-sass/_drawing.scss -index d98cb49397..f1e82176a4 100644 +index d98cb4939..f1e82176a 100644 --- a/data/theme/gnome-shell-sass/_drawing.scss +++ b/data/theme/gnome-shell-sass/_drawing.scss @@ -219,6 +219,7 @@ @@ -46,7 +46,7 @@ index d98cb49397..f1e82176a4 100644 // background color overrides for notification style diff --git a/data/theme/gnome-shell-sass/widgets/_login-lock.scss b/data/theme/gnome-shell-sass/widgets/_login-lock.scss -index b661e93c8d..6cca1e28e9 100644 +index b661e93c8..6cca1e28e 100644 --- a/data/theme/gnome-shell-sass/widgets/_login-lock.scss +++ b/data/theme/gnome-shell-sass/widgets/_login-lock.scss @@ -137,12 +137,7 @@ $_gdm_dialog_width: 25em; @@ -80,31 +80,56 @@ index b661e93c8d..6cca1e28e9 100644 padding: $base_padding * 1.5; -- -2.53.0 +2.54.0 -From bfcebf4a5a60c64e5cbec78dbb8b646b69b571da Mon Sep 17 00:00:00 2001 +From 449e8ad3d5558cd00c3dae0d8ca99ef96acb8456 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Thu, 12 Feb 2026 16:37:08 +0100 -Subject: [PATCH 02/42] unlockDialog: Vertically center dialog using fixed +Subject: [PATCH 02/54] 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. ---- - js/ui/unlockDialog.js | 20 +++++++++++++++----- - 1 file changed, 15 insertions(+), 5 deletions(-) +Position the topY of the content based on a fixed prompt height estimation. +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. +--- + data/theme/gnome-shell-sass/widgets/_login-lock.scss | 2 ++ + js/ui/unlockDialog.js | 10 ++++++---- + 2 files changed, 8 insertions(+), 4 deletions(-) + +diff --git a/data/theme/gnome-shell-sass/widgets/_login-lock.scss b/data/theme/gnome-shell-sass/widgets/_login-lock.scss +index 6cca1e28e..46805ebca 100644 +--- a/data/theme/gnome-shell-sass/widgets/_login-lock.scss ++++ b/data/theme/gnome-shell-sass/widgets/_login-lock.scss +@@ -14,6 +14,7 @@ $_gdm_dialog_width: 25em; + + .login-dialog-prompt-layout { + width: $_gdm_dialog_width; ++ margin-top: 80px; + spacing: $base_padding * 1.5; + } + } +@@ -226,6 +227,7 @@ $_gdm_dialog_width: 25em; + .unlock-dialog-clock { + color: $_gdm_fg; + spacing: 2em; ++ margin-top: 150px; + + .unlock-dialog-clock-time { + @extend %numeric; diff --git a/js/ui/unlockDialog.js b/js/ui/unlockDialog.js -index 63ba591eec..5d14cb9555 100644 +index 63ba591ee..a014095bb 100644 --- a/js/ui/unlockDialog.js +++ b/js/ui/unlockDialog.js @@ -33,6 +33,8 @@ const FADE_OUT_SCALE = 0.3; const BLUR_BRIGHTNESS = 0.65; const BLUR_RADIUS = 90; -+const FIXED_PROMPT_HEIGHT = 400; ++const FIXED_PROMPT_HEIGHT = 550; + const NotificationsBox = GObject.registerClass({ Signals: {'wake-up-screen': {}}, @@ -120,35 +145,25 @@ index 63ba591eec..5d14cb9555 100644 let [, , stackWidth, stackHeight] = this._stack.get_preferred_size(); -@@ -476,9 +478,17 @@ class UnlockDialogLayout extends Clutter.LayoutManager { +@@ -476,8 +478,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.53.0 +2.54.0 -From 6873ad575042d38fb5ab0663728694110db60c4a Mon Sep 17 00:00:00 2001 +From dbdc98e1cb1adc03c30d701aa1eaed0257967453 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Thu, 12 Feb 2026 16:38:04 +0100 -Subject: [PATCH 03/42] unlockDialog: Fix username reuse on reset +Subject: [PATCH 03/54] 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 @@ -158,10 +173,10 @@ instead to correctly reuse the username when requested. 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/ui/unlockDialog.js b/js/ui/unlockDialog.js -index 5d14cb9555..b6d71cd47f 100644 +index a014095bb..420dae4c6 100644 --- a/js/ui/unlockDialog.js +++ b/js/ui/unlockDialog.js -@@ -848,7 +848,7 @@ export const UnlockDialog = GObject.registerClass({ +@@ -840,7 +840,7 @@ export const UnlockDialog = GObject.registerClass({ _onReset(authPrompt, beginRequest) { let userName; @@ -171,45 +186,75 @@ index 5d14cb9555..b6d71cd47f 100644 userName = this._userName; } else { -- -2.53.0 +2.54.0 -From c3467f84952ae46add711482d121969c9c55633d Mon Sep 17 00:00:00 2001 +From a3696c5975c32da5afe20b4cf1fe1388f579322e Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Thu, 12 Feb 2026 16:38:54 +0100 -Subject: [PATCH 04/42] unlockDialog: Wait for authPrompt destruction before +Subject: [PATCH 04/54] 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. --- - js/ui/unlockDialog.js | 4 ++-- - 1 file changed, 2 insertions(+), 2 deletions(-) + 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 b6d71cd47f..02c302ce7a 100644 +index 420dae4c6..7a02f68ee 100644 --- a/js/ui/unlockDialog.js +++ b/js/ui/unlockDialog.js -@@ -898,8 +898,8 @@ export const UnlockDialog = GObject.registerClass({ +@@ -890,8 +890,7 @@ export const UnlockDialog = GObject.registerClass({ } _otherUserClicked() { - Gdm.goto_login_session_sync(null); - -+ this._authPrompt.connectObject('destroy', () => -+ Gdm.goto_login_session_sync(null)); ++ this._authPrompt.connect('destroy', () => Gdm.goto_login_session_sync(null)); this._authPrompt.cancel(); } -- -2.53.0 +2.54.0 -From 0c642b409fab1374dd466a64b450d4a67e151452 Mon Sep 17 00:00:00 2001 +From a0c1490f2312b1594430fc0a16916e8ec93bbe9f Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Tue, 10 Mar 2026 13:56:02 +0100 +Subject: [PATCH 05/54] authPrompt: Separate input well styles from prompt + layout + +Give _inputWell its own style class instead of reusing +login-dialog-prompt-layout. The prompt layout defines the width, +while the input well just fills horizontally within it. +--- + data/theme/gnome-shell-sass/widgets/_login-lock.scss | 3 +++ + 1 file changed, 3 insertions(+) + +diff --git a/data/theme/gnome-shell-sass/widgets/_login-lock.scss b/data/theme/gnome-shell-sass/widgets/_login-lock.scss +index 46805ebca..ca8d295ce 100644 +--- a/data/theme/gnome-shell-sass/widgets/_login-lock.scss ++++ b/data/theme/gnome-shell-sass/widgets/_login-lock.scss +@@ -15,6 +15,9 @@ $_gdm_dialog_width: 25em; + .login-dialog-prompt-layout { + width: $_gdm_dialog_width; + margin-top: 80px; ++ } ++ ++ .login-dialog-input-well { + spacing: $base_padding * 1.5; + } + } +-- +2.54.0 + + +From 706c9c84b33d267172e57e4d67f2152b1a243680 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Thu, 5 Feb 2026 18:55:38 +0100 -Subject: [PATCH 05/42] authPrompt: Use destructured object for +Subject: [PATCH 06/54] authPrompt: Use destructured object for updateSensitivity Replace the boolean parameter with a destructured object to make @@ -221,7 +266,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 3b4a2f7988..526dcdbaa0 100644 +index 3b4a2f798..526dcdbaa 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js @@ -287,7 +287,7 @@ export const AuthPrompt = GObject.registerClass({ @@ -303,7 +348,7 @@ index 3b4a2f7988..526dcdbaa0 100644 let hold = params.hold; if (!hold) diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js -index 5aca896db9..9f76464ad5 100644 +index 5aca896db..9f76464ad 100644 --- a/js/gdm/loginDialog.js +++ b/js/gdm/loginDialog.js @@ -1148,8 +1148,8 @@ export const LoginDialog = GObject.registerClass({ @@ -327,10 +372,10 @@ index 5aca896db9..9f76464ad5 100644 } diff --git a/js/ui/unlockDialog.js b/js/ui/unlockDialog.js -index 02c302ce7a..7f4082b92a 100644 +index 7a02f68ee..85e469fb8 100644 --- a/js/ui/unlockDialog.js +++ b/js/ui/unlockDialog.js -@@ -765,7 +765,7 @@ export const UnlockDialog = GObject.registerClass({ +@@ -757,7 +757,7 @@ export const UnlockDialog = GObject.registerClass({ case AuthPromptStatus.VERIFICATION_FAILED: this._authPrompt.reset(); this._authPrompt.updateSensitivity( @@ -340,13 +385,13 @@ index 02c302ce7a..7f4082b92a 100644 } -- -2.53.0 +2.54.0 -From f30150c922a3aa4fd97101f9a2b0362ba84a029a Mon Sep 17 00:00:00 2001 +From 7e7846011e53d45e7c532383fff5d213034bbbd5 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Thu, 12 Feb 2026 16:47:22 +0100 -Subject: [PATCH 06/42] authPrompt: Use array-based widget lookup in +Subject: [PATCH 07/54] authPrompt: Use array-based widget lookup in updateSensitivity Replace the if/else widget selection with array-based lookup to @@ -356,7 +401,7 @@ prepare for additional auth widgets in upcoming commits. 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index 526dcdbaa0..7967b60a81 100644 +index 526dcdbaa..7967b60a8 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js @@ -612,12 +612,9 @@ export const AuthPrompt = GObject.registerClass({ @@ -376,29 +421,76 @@ index 526dcdbaa0..7967b60a81 100644 if (authWidget.reactive === sensitive) return; -- -2.53.0 +2.54.0 -From 356968d5a09cca83d0cc7ac53774fd42092ae042 Mon Sep 17 00:00:00 2001 +From 76e63f00fe54ed5c13a16bab4c3a3edd6d788d4b Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Tue, 10 Mar 2026 17:10:58 +0100 +Subject: [PATCH 08/54] authPrompt: Generalize _fadeInChoiceList to accept any + 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. +--- + js/gdm/authPrompt.js | 11 +++++++---- + 1 file changed, 7 insertions(+), 4 deletions(-) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index 7967b60a8..f19e639a7 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -535,13 +535,16 @@ export const AuthPrompt = GObject.registerClass({ + this._entry.grab_key_focus(); + } + +- _fadeInChoiceList() { +- this._authList.set({ ++ _fadeInElement(element) { ++ if (element.visible) ++ return; ++ ++ element.set({ + opacity: 0, + visible: true, + }); + this.updateSensitivity({sensitive: false}); +- this._authList.ease({ ++ element.ease({ + opacity: 255, + duration: MESSAGE_FADE_OUT_ANIMATION_TIME, + transition: Clutter.AnimationMode.EASE_OUT_QUAD, +@@ -560,7 +563,7 @@ export const AuthPrompt = GObject.registerClass({ + this._entry.hide(); + if (this._message.text === '') + this._message.hide(); +- this._fadeInChoiceList(); ++ this._fadeInElement(this._authList); + } + + getAnswer() { +-- +2.54.0 + + +From be6debccbd80cd595a725c6d3081313bd0275940 Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Fri, 9 Feb 2024 09:02:25 -0500 -Subject: [PATCH 07/42] authPrompt: Fade out input buttons/entry after +Subject: [PATCH 09/54] 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. - -Also, make _fadeInElement more abstract instead of specific for -authList. It'll be used in next commits. --- - js/gdm/authPrompt.js | 25 +++++++++++++++++++------ + js/gdm/authPrompt.js | 17 +++++++++++++++-- js/gdm/loginDialog.js | 11 +++++++++++ - 2 files changed, 30 insertions(+), 6 deletions(-) + 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index 7967b60a81..33d46feba8 100644 +index f19e639a7..e2ae3b047 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js @@ -51,6 +51,7 @@ export const AuthPrompt = GObject.registerClass({ @@ -439,34 +531,8 @@ index 7967b60a81..33d46feba8 100644 } setQuestion(question) { -@@ -535,13 +548,13 @@ export const AuthPrompt = GObject.registerClass({ - this._entry.grab_key_focus(); - } - -- _fadeInChoiceList() { -- this._authList.set({ -+ _fadeInElement(element) { -+ element.set({ - opacity: 0, - visible: true, - }); - this.updateSensitivity({sensitive: false}); -- this._authList.ease({ -+ element.ease({ - opacity: 255, - duration: MESSAGE_FADE_OUT_ANIMATION_TIME, - transition: Clutter.AnimationMode.EASE_OUT_QUAD, -@@ -560,7 +573,7 @@ export const AuthPrompt = GObject.registerClass({ - this._entry.hide(); - if (this._message.text === '') - this._message.hide(); -- this._fadeInChoiceList(); -+ this._fadeInElement(this._authList); - } - - getAnswer() { diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js -index 9f76464ad5..f75497065c 100644 +index 9f76464ad..f75497065 100644 --- a/js/gdm/loginDialog.js +++ b/js/gdm/loginDialog.js @@ -590,6 +590,7 @@ export const LoginDialog = GObject.registerClass({ @@ -495,13 +561,13 @@ index 9f76464ad5..f75497065c 100644 this._sessionMenuButton.setActiveSession(sessionId); } -- -2.53.0 +2.54.0 -From 34e3c9dc1d829860eccb5a75e34b6d0c9d6c35dc Mon Sep 17 00:00:00 2001 +From 19705a097a11e545a20703bdce7700ba6bb09477 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Wed, 21 Jan 2026 14:23:34 +0100 -Subject: [PATCH 08/42] authPrompt: Don't reset preemptiveAnswer when +Subject: [PATCH 10/54] authPrompt: Don't reset preemptiveAnswer when VERIFICATION_IN_PROGRESS PreemptiveAnswer wasn't being used in the case where verification is in @@ -514,10 +580,10 @@ service asks for the PIN. 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index 33d46feba8..ec3c2ec546 100644 +index e2ae3b047..eb7f36821 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -685,7 +685,8 @@ export const AuthPrompt = GObject.registerClass({ +@@ -688,7 +688,8 @@ export const AuthPrompt = GObject.registerClass({ this.verificationStatus = AuthPromptStatus.NOT_VERIFYING; this.cancelButton.reactive = this._hasCancelButton; this.cancelButton.can_focus = this._hasCancelButton; @@ -528,13 +594,13 @@ index 33d46feba8..ec3c2ec546 100644 if (this._preemptiveAnswerWatchId) this._idleMonitor.remove_watch(this._preemptiveAnswerWatchId); -- -2.53.0 +2.54.0 -From fdd96fe191abfaea4fd29242fd7df8e78200a17e Mon Sep 17 00:00:00 2001 +From e7da2b6729fb2585f6f0d02099a8b9af2e7b9139 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Thu, 12 Feb 2026 17:05:32 +0100 -Subject: [PATCH 09/42] style: Increase hint-text left margin +Subject: [PATCH 11/54] 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. @@ -543,7 +609,7 @@ Increase the left margin to ensure proper readability. 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 41e10f7663..5a45e86d6d 100644 +index 41e10f766..5a45e86d6 100644 --- a/data/theme/gnome-shell-sass/widgets/_entries.scss +++ b/data/theme/gnome-shell-sass/widgets/_entries.scss @@ -15,6 +15,6 @@ StEntry { @@ -555,23 +621,23 @@ index 41e10f7663..5a45e86d6d 100644 } } -- -2.53.0 +2.54.0 -From 2532bcfc85315b588f4c5c6ddaee7a3287fa8911 Mon Sep 17 00:00:00 2001 +From 005ef41251c30e20cbaa8ec2a235ea7b0bc69ccb Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Mon, 16 Feb 2026 16:05:17 +0100 -Subject: [PATCH 10/42] authPrompt: Use connectObject for userVerifier signals +Subject: [PATCH 12/54] 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. --- - js/gdm/authPrompt.js | 19 +++++++++++-------- - 1 file changed, 11 insertions(+), 8 deletions(-) + js/gdm/authPrompt.js | 20 +++++++++++--------- + 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index ec3c2ec546..5a62e8897f 100644 +index eb7f36821..bf1b8aea5 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js @@ -80,14 +80,16 @@ export const AuthPrompt = GObject.registerClass({ @@ -599,40 +665,52 @@ index ec3c2ec546..5a62e8897f 100644 this.smartcardDetected = this._userVerifier.smartcardDetected; this.connect('destroy', this._onDestroy.bind(this)); -@@ -141,6 +143,7 @@ export const AuthPrompt = GObject.registerClass({ +@@ -140,7 +142,7 @@ export const AuthPrompt = GObject.registerClass({ + this._inactiveEntry.destroy(); this._inactiveEntry = null; - +- + this._userVerifier.disconnectObject(this); this._userVerifier.destroy(); this._userVerifier = null; this._entry = null; -- -2.53.0 +2.54.0 -From f634f1e7ff47de5a3a042b0da4d7af6b06f6831f Mon Sep 17 00:00:00 2001 +From be65b189e704c9273c5c841674a72e43586ae587 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Thu, 12 Feb 2026 17:20:34 +0100 -Subject: [PATCH 11/42] authPrompt: Redesign entry area layout +Subject: [PATCH 13/54] authPrompt: Update entry layout based on mockups -Restructure the authentication prompt layout: -- Make the entry larger and more rounded. -- Add a new next button, it's inside _defaultButtonWell. -- Wrap _entry and _defaultButtonWell in a new _entryArea container. -- _defaultButtonWell now is overlaying _entryArea aligned on the right. -- Anchor cancelButton to _entry using a constraint. +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 next button and the +spinner appear inside the entry. + +The entry is horizontally centered as username and avatar. The +cancelButton is placed on the left of the centered entry. --- - .../gnome-shell-sass/widgets/_login-lock.scss | 24 ++++++- - js/gdm/authPrompt.js | 68 ++++++++++++++----- - 2 files changed, 72 insertions(+), 20 deletions(-) + .../gnome-shell-sass/widgets/_login-lock.scss | 30 +++++++-- + js/gdm/authPrompt.js | 63 ++++++++++++++----- + 2 files changed, 75 insertions(+), 18 deletions(-) diff --git a/data/theme/gnome-shell-sass/widgets/_login-lock.scss b/data/theme/gnome-shell-sass/widgets/_login-lock.scss -index 6cca1e28e9..93dbe617b7 100644 +index ca8d295ce..7024cc58d 100644 --- a/data/theme/gnome-shell-sass/widgets/_login-lock.scss +++ b/data/theme/gnome-shell-sass/widgets/_login-lock.scss -@@ -16,6 +16,19 @@ $_gdm_dialog_width: 25em; - width: $_gdm_dialog_width; +@@ -13,13 +13,29 @@ $_gdm_dialog_width: 25em; + } + + .login-dialog-prompt-layout { +- width: $_gdm_dialog_width; ++ width: $_gdm_dialog_width * 1.2; + margin-top: 80px; + } + + .login-dialog-input-well { spacing: $base_padding * 1.5; } + @@ -642,16 +720,19 @@ index 6cca1e28e9..93dbe617b7 100644 + + .login-dialog-prompt-entry { + border-radius: $base_border_radius * 1.5; -+ padding-right: 3em; // 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: 1em; ++ :ltr { margin-right: 0.5em; } ++ :rtl { margin-left: 0.5em; } + } } // GDM Login Dialog -@@ -33,7 +46,7 @@ $_gdm_dialog_width: 25em; +@@ -37,7 +53,7 @@ $_gdm_dialog_width: 25em; // buttons on login screen .login-dialog-button { @@ -660,7 +741,7 @@ index 6cca1e28e9..93dbe617b7 100644 &.a11y-button, &.cancel-button, &.switch-user-button, -@@ -44,13 +57,18 @@ $_gdm_dialog_width: 25em; +@@ -48,13 +64,19 @@ $_gdm_dialog_width: 25em; padding: to_em(16px); } @@ -672,6 +753,7 @@ index 6cca1e28e9..93dbe617b7 100644 &.cancel-button { - padding: $base_padding * 1.5; + padding: $base_padding * 2; ++ margin: 0.5em 0; } } @@ -682,50 +764,10 @@ index 6cca1e28e9..93dbe617b7 100644 .conflicting-session-dialog-content { diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index 5a62e8897f..474b6ecd9c 100644 +index bf1b8aea5..4367ca1de 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -2,6 +2,7 @@ import Clutter from 'gi://Clutter'; - import GLib from 'gi://GLib'; - import Atk from 'gi://Atk'; - import GObject from 'gi://GObject'; -+import Graphene from 'gi://Graphene'; - import Pango from 'gi://Pango'; - import Shell from 'gi://Shell'; - import St from 'gi://St'; -@@ -158,9 +159,11 @@ export const 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', -- orientation: Clutter.Orientation.HORIZONTAL, -+ x_expand: true, -+ y_expand: false, - }); - this.add_child(this._mainBox); - -@@ -170,10 +173,17 @@ export const 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, - 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 -@@ -201,10 +211,20 @@ export const AuthPrompt = GObject.registerClass({ +@@ -200,10 +200,20 @@ export const AuthPrompt = GObject.registerClass({ }); this._mainBox.add_child(this._authList); @@ -747,17 +789,16 @@ index 5a62e8897f..474b6ecd9c 100644 }; this._entry = null; -@@ -216,8 +236,7 @@ export const AuthPrompt = GObject.registerClass({ +@@ -215,7 +225,7 @@ export const 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._inactiveEntry = this._textEntry; - this._timedLoginIndicator = new St.Bin({ -@@ -242,17 +261,28 @@ export const AuthPrompt = GObject.registerClass({ +@@ -241,17 +251,38 @@ export const AuthPrompt = GObject.registerClass({ this._defaultButtonWell = new St.Widget({ layout_manager: new Clutter.BinLayout(), @@ -788,10 +829,20 @@ index 5a62e8897f..474b6ecd9c 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 St.Widget({ ++ constraints: new Clutter.BindConstraint({ ++ source: this.cancelButton, ++ coordinate: Clutter.BindCoordinate.WIDTH, ++ }), ++ })); } showTimedLoginIndicator(time) { -@@ -322,7 +352,7 @@ export const AuthPrompt = GObject.registerClass({ +@@ -321,7 +352,7 @@ export const AuthPrompt = GObject.registerClass({ } if (newEntry) { @@ -800,7 +851,7 @@ index 5a62e8897f..474b6ecd9c 100644 this._entry = newEntry; this._inactiveEntry = inactiveEntry; -@@ -424,17 +454,17 @@ export const AuthPrompt = GObject.registerClass({ +@@ -423,17 +454,17 @@ export const AuthPrompt = GObject.registerClass({ } this.updateSensitivity({sensitive: canRetry}); @@ -821,7 +872,7 @@ index 5a62e8897f..474b6ecd9c 100644 this.verificationStatus = AuthPromptStatus.VERIFICATION_SUCCEEDED; this._mainBox.reactive = false; -@@ -547,7 +577,8 @@ export const AuthPrompt = GObject.registerClass({ +@@ -546,7 +577,8 @@ export const AuthPrompt = GObject.registerClass({ this._entry.hint_text = question; this._authList.hide(); @@ -831,7 +882,7 @@ index 5a62e8897f..474b6ecd9c 100644 this._entry.grab_key_focus(); } -@@ -573,7 +604,7 @@ export const AuthPrompt = GObject.registerClass({ +@@ -575,7 +607,7 @@ export const AuthPrompt = GObject.registerClass({ this._authList.addItem(key, text); } @@ -840,7 +891,7 @@ index 5a62e8897f..474b6ecd9c 100644 if (this._message.text === '') this._message.hide(); this._fadeInElement(this._authList); -@@ -635,6 +666,9 @@ export const AuthPrompt = GObject.registerClass({ +@@ -637,6 +669,9 @@ export const AuthPrompt = GObject.registerClass({ if (authWidget.reactive === sensitive) return; @@ -850,7 +901,7 @@ index 5a62e8897f..474b6ecd9c 100644 authWidget.reactive = sensitive; if (sensitive) { -@@ -648,7 +682,7 @@ export const AuthPrompt = GObject.registerClass({ +@@ -650,7 +685,7 @@ export const AuthPrompt = GObject.registerClass({ } vfunc_hide() { @@ -860,22 +911,31 @@ index 5a62e8897f..474b6ecd9c 100644 this._message.opacity = 0; -- -2.53.0 +2.54.0 -From e44919241a35ba1a0e515cf8b05f181dd0506688 Mon Sep 17 00:00:00 2001 +From 5aae5b3b70845a57e74efeef18ca5c3465d52838 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Thu, 12 Feb 2026 17:41:48 +0100 -Subject: [PATCH 12/42] authPrompt: Now by default the entry area fades in +Subject: [PATCH 14/54] authPrompt: Fade in _entryArea instead of abruptly + showing it -This means when clear it's hidden. Keep it visible on verification failed, -it'll be cleared later on reset. +To support the new fade-in flow, the _entryArea is now hidden by default. +The clear() function has been updated to reset this state to hidden. + +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(). --- 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 474b6ecd9c..d639d574e4 100644 +index 4367ca1de..9e0557c2f 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js @@ -384,7 +384,6 @@ export const AuthPrompt = GObject.registerClass({ @@ -917,13 +977,13 @@ index 474b6ecd9c..d639d574e4 100644 _fadeInElement(element) { -- -2.53.0 +2.54.0 -From 67209dda9cfc1be9a87b2765406a5a8e7bd11af8 Mon Sep 17 00:00:00 2001 +From b3b58f92b7f62c15c2b52a4725e501f30ec4932e Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Thu, 12 Feb 2026 17:36:08 +0100 -Subject: [PATCH 13/42] authPrompt: Show entry area when displaying message +Subject: [PATCH 15/54] 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. @@ -933,59 +993,47 @@ This allows getting a preemptive answer. 1 file changed, 8 insertions(+) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index d639d574e4..d876a3acf5 100644 +index 9e0557c2f..2e3084535 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -441,6 +441,14 @@ export const AuthPrompt = GObject.registerClass({ - } +@@ -442,6 +442,14 @@ export const AuthPrompt = GObject.registerClass({ 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._fadeInElement(this._entryArea); -+ - this.emit('prompted'); } + _onVerificationFailed(userVerifier, serviceName, canRetry) { -- -2.53.0 +2.54.0 -From ae7d20481b14b922d45c20e3d02585ca0526e390 Mon Sep 17 00:00:00 2001 +From 040dcd66356f235e965c7ab15ea7f79b4c9b3119 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez -Date: Thu, 12 Feb 2026 17:28:54 +0100 -Subject: [PATCH 14/42] authPrompt: Refactor button-well animation and add - loading signal +Date: Tue, 19 May 2026 10:21:50 +0200 +Subject: [PATCH 16/54] authPrompt: Add loading signal -- Simplify setActorInDefaultButtonWell animation logic using start/stop - spinning. -- Simplify _activateNext checking internally reactive state and - passwordEntry. -- Add 'loading' signal emitted on start/stop spinning (used in upcoming - commits). -- 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()`. --- - js/gdm/authPrompt.js | 105 +++++++++++++++++++------------------------ - 1 file changed, 46 insertions(+), 59 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 d876a3acf5..1f78b70bd4 100644 +index 2e3084535..aed460181 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -17,7 +17,6 @@ import * as UserWidget from '../ui/userWidget.js'; - import {wiggle} from '../misc/animationUtils.js'; - - const DEFAULT_BUTTON_WELL_ICON_SIZE = 16; --const DEFAULT_BUTTON_WELL_ANIMATION_DELAY = 1000; - const DEFAULT_BUTTON_WELL_ANIMATION_TIME = 300; - - const MESSAGE_FADE_OUT_ANIMATION_TIME = 500; -@@ -53,6 +52,7 @@ export const AuthPrompt = GObject.registerClass({ +@@ -52,6 +52,7 @@ export const AuthPrompt = GObject.registerClass({ 'prompted': {}, 'reset': {param_types: [GObject.TYPE_UINT]}, 'verification-complete': {}, @@ -993,7 +1041,118 @@ index d876a3acf5..1f78b70bd4 100644 }, }, class AuthPrompt extends St.BoxLayout { _init(gdmClient, mode) { -@@ -252,11 +252,7 @@ export const AuthPrompt = GObject.registerClass({ +@@ -459,7 +460,7 @@ export const 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 @@ export const AuthPrompt = GObject.registerClass({ + } + + _onVerificationComplete() { +- this.setActorInDefaultButtonWell(this._nextButton, true); ++ this.stopSpinning({animate: true}); + this.verificationStatus = AuthPromptStatus.VERIFICATION_SUCCEEDED; + + this._mainBox.reactive = false; +@@ -553,12 +554,14 @@ export const 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() { +@@ -690,7 +693,7 @@ export const AuthPrompt = GObject.registerClass({ + } + + vfunc_hide() { +- this.setActorInDefaultButtonWell(this._nextButton, true); ++ this.stopSpinning(); + super.vfunc_hide(); + this._message.opacity = 0; + +-- +2.54.0 + + +From 9e1d08ac5961c227d468acb0ee7ae3d7889de0a4 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Tue, 19 May 2026 10:24:38 +0200 +Subject: [PATCH 17/54] authPrompt: Don't delay spinner + +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. +--- + js/gdm/authPrompt.js | 4 +--- + 1 file changed, 1 insertion(+), 3 deletions(-) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index aed460181..14bd6048d 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -16,7 +16,6 @@ import * as UserWidget from '../ui/userWidget.js'; + import {wiggle} from '../misc/animationUtils.js'; + + const DEFAULT_BUTTON_WELL_ICON_SIZE = 16; +-const DEFAULT_BUTTON_WELL_ANIMATION_DELAY = 1000; + const DEFAULT_BUTTON_WELL_ANIMATION_TIME = 300; + + const MESSAGE_FADE_OUT_ANIMATION_TIME = 500; +@@ -523,7 +522,6 @@ export const 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) { +@@ -545,7 +543,7 @@ export const 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.54.0 + + +From 6083120cf9c832d0abc55495c3c590b49bc44da2 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Thu, 12 Feb 2026 17:28:54 +0100 +Subject: [PATCH 18/54] authPrompt: Simplify some code + +--- + js/gdm/authPrompt.js | 92 +++++++++++++++++++------------------------- + 1 file changed, 39 insertions(+), 53 deletions(-) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index 14bd6048d..ff9a7252b 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -242,11 +242,7 @@ export const AuthPrompt = GObject.registerClass({ this._fadeOutMessage(); }); @@ -1006,7 +1165,7 @@ index d876a3acf5..1f78b70bd4 100644 }); this._defaultButtonWell = new St.Widget({ -@@ -319,12 +315,15 @@ export const AuthPrompt = GObject.registerClass({ +@@ -319,13 +315,16 @@ export const AuthPrompt = GObject.registerClass({ this._timedLoginIndicator.scale_x = 0.; } @@ -1020,29 +1179,13 @@ index d876a3acf5..1f78b70bd4 100644 if (this._queryingService) { - if (shouldSpin) +- this.startSpinning(); + if (this._entry === this._passwordEntry) - this.startSpinning(); ++ this.startSpinning({animate: true}); this._userVerifier.answerQuery(this._queryingService, this._entry.text); -@@ -459,7 +458,7 @@ export const 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 +468,7 @@ export const AuthPrompt = GObject.registerClass({ - } - - _onVerificationComplete() { -- this.setActorInDefaultButtonWell(this._nextButton, true); -+ this.stopSpinning(true); - this.verificationStatus = AuthPromptStatus.VERIFICATION_SUCCEEDED; - - this._mainBox.reactive = false; -@@ -489,76 +488,64 @@ export const AuthPrompt = GObject.registerClass({ + } else { +@@ -489,64 +488,51 @@ export const AuthPrompt = GObject.registerClass({ } setActorInDefaultButtonWell(actor, animate) { @@ -1085,7 +1228,6 @@ index d876a3acf5..1f78b70bd4 100644 - oldActor.ease({ - opacity: 0, - duration: DEFAULT_BUTTON_WELL_ANIMATION_TIME, -- delay: DEFAULT_BUTTON_WELL_ANIMATION_DELAY, - mode: Clutter.AnimationMode.LINEAR, - onComplete: () => { - if (wasSpinner) { @@ -1126,7 +1268,7 @@ index d876a3acf5..1f78b70bd4 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, - }); - } @@ -1140,38 +1282,79 @@ index d876a3acf5..1f78b70bd4 100644 } this._defaultButtonWellActor = actor; - } - - startSpinning() { -+ this.emit('loading', true); - this.setActorInDefaultButtonWell(this._spinner, true); - } - -- stopSpinning() { -- this.setActorInDefaultButtonWell(null, false); -+ stopSpinning(animate) { -+ this.emit('loading', false); -+ this.setActorInDefaultButtonWell(this._nextButton, animate); - } - - clear() { -@@ -687,7 +674,7 @@ export const AuthPrompt = GObject.registerClass({ - } - - vfunc_hide() { -- this.setActorInDefaultButtonWell(this._nextButton, true); -+ this.stopSpinning(); - super.vfunc_hide(); - this._message.opacity = 0; - -- -2.53.0 +2.54.0 -From b502b7c6e06a77c17ba225e91539f60118e39328 Mon Sep 17 00:00:00 2001 +From cdb58710428b47550fe4aa489287fe705f5462fe Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Tue, 26 May 2026 16:48:43 +0200 +Subject: [PATCH 19/54] authPrompt: Ensure this._message is always visible + +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. +--- + js/gdm/authPrompt.js | 3 --- + 1 file changed, 3 deletions(-) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index ff9a7252b..935a1ab6c 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -600,8 +600,6 @@ export const AuthPrompt = GObject.registerClass({ + } + + this._entryArea.hide(); +- if (this._message.text === '') +- this._message.hide(); + this._fadeInElement(this._authList); + } + +@@ -640,7 +638,6 @@ export const 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.54.0 + + +From 42df2462df05911e61613d8a8f4b7dd82861fb84 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Tue, 10 Mar 2026 18:32:48 +0100 +Subject: [PATCH 20/54] authPrompt: Skip reset after successful verification + +Don't reset the prompt if verification has already succeeded. +--- + js/gdm/authPrompt.js | 3 +++ + 1 file changed, 3 insertions(+) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index 935a1ab6c..692fc62cf 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -483,6 +483,9 @@ export const AuthPrompt = GObject.registerClass({ + } + + _onReset() { ++ if (this.verificationStatus === AuthPromptStatus.VERIFICATION_SUCCEEDED) ++ return; ++ + this.verificationStatus = AuthPromptStatus.NOT_VERIFYING; + this.reset(); + } +-- +2.54.0 + + +From 281a61563e57726e0e1583b5978b22a9c41cda35 Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Tue, 6 Feb 2024 13:06:32 -0500 -Subject: [PATCH 15/42] authPrompt: Parameterize reset function +Subject: [PATCH 21/54] authPrompt: Parameterize reset function In the future, userVerifier will request a partial reset where some state is carried over or explicitly specified. @@ -1179,50 +1362,53 @@ 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. --- - js/gdm/authPrompt.js | 35 +++++++++++++++++++++++++---------- - 1 file changed, 25 insertions(+), 10 deletions(-) + js/gdm/authPrompt.js | 36 ++++++++++++++++++++++++------------ + 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index 1f78b70bd4..2614b82410 100644 +index 692fc62cf..541927655 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -482,9 +482,11 @@ export const AuthPrompt = GObject.registerClass({ +@@ -482,12 +482,11 @@ export const AuthPrompt = GObject.registerClass({ this.emit('verification-complete'); } - _onReset() { ++ _onReset(_userVerifier, resetParams) { + if (this.verificationStatus === AuthPromptStatus.VERIFICATION_SUCCEEDED) + return; + - this.verificationStatus = AuthPromptStatus.NOT_VERIFYING; - this.reset(); -+ _onReset(_, resetParams) { -+ if (this.verificationStatus === AuthPromptStatus.VERIFICATION_SUCCEEDED) -+ return; -+ + this.reset(resetParams); } setActorInDefaultButtonWell(actor, animate) { -@@ -548,10 +550,17 @@ export const AuthPrompt = GObject.registerClass({ +@@ -551,11 +550,18 @@ export const 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(); -@@ -709,8 +718,13 @@ export const AuthPrompt = GObject.registerClass({ + +@@ -712,8 +718,13 @@ export const AuthPrompt = GObject.registerClass({ this.updateSensitivity(false); } @@ -1238,7 +1424,7 @@ index 1f78b70bd4..2614b82410 100644 this.verificationStatus = AuthPromptStatus.NOT_VERIFYING; this.cancelButton.reactive = this._hasCancelButton; this.cancelButton.can_focus = this._hasCancelButton; -@@ -726,7 +740,7 @@ export const AuthPrompt = GObject.registerClass({ +@@ -729,7 +740,7 @@ export const AuthPrompt = GObject.registerClass({ this._userVerifier.cancel(); this._queryingService = null; @@ -1247,7 +1433,7 @@ index 1f78b70bd4..2614b82410 100644 this._message.opacity = 0; this.setUser(null); this._updateEntry(true); -@@ -749,7 +763,8 @@ export const AuthPrompt = GObject.registerClass({ +@@ -752,7 +763,8 @@ export const AuthPrompt = GObject.registerClass({ // We don't need to know the username if the user preempted the login screen // with a smartcard or with preauthenticated oVirt credentials beginRequestType = BeginRequestType.DONT_PROVIDE_USERNAME; @@ -1258,13 +1444,69 @@ index 1f78b70bd4..2614b82410 100644 beginRequestType = BeginRequestType.REUSE_USERNAME; } else { -- -2.53.0 +2.54.0 -From 090803f771f3c1b800ac256c0b91b50f07c12813 Mon Sep 17 00:00:00 2001 +From 9e6a60dd50153ca81c2bd938340f5672f4600f38 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Tue, 19 May 2026 15:18:23 +0200 +Subject: [PATCH 22/54] authPrompt: Remove this.stopSpinnning() in reset + +It's already in this.clear() which is called from reset. +--- + js/gdm/authPrompt.js | 1 - + 1 file changed, 1 deletion(-) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index 541927655..948dffc13 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -744,7 +744,6 @@ export const AuthPrompt = GObject.registerClass({ + this._message.opacity = 0; + this.setUser(null); + this._updateEntry(true); +- this.stopSpinning(); + + if (oldStatus === AuthPromptStatus.VERIFICATION_FAILED) + this.emit('failed'); +-- +2.54.0 + + +From f4fcb1738f2398f7486e30be6c093b3d770febcc Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Tue, 21 Apr 2026 13:06:57 +0200 +Subject: [PATCH 23/54] authPrompt: Ensure hint_text in entries is cleared + +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. +--- + js/gdm/authPrompt.js | 2 ++ + 1 file changed, 2 insertions(+) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index 948dffc13..5d745db76 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -555,6 +555,8 @@ export const AuthPrompt = GObject.registerClass({ + reuseEntryText: false, + }); + ++ this._entry.hint_text = ''; ++ this._inactiveEntry.hint_text = ''; + if (!reuseEntryText) { + this._entryArea.hide(); + this._entry.text = ''; +-- +2.54.0 + + +From 383b002b0fb9c654ef17111c0364d436de70c91c Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Thu, 12 Feb 2026 16:56:48 +0100 -Subject: [PATCH 16/42] authPrompt: Rename BeginRequestType to ResetType +Subject: [PATCH 24/54] 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. @@ -1275,10 +1517,10 @@ of reset being performed, so ResetType is a clearer name. 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index 2614b82410..ea09f470b4 100644 +index 5d745db76..7f3752d82 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -38,7 +38,7 @@ export const AuthPromptStatus = { +@@ -37,7 +37,7 @@ export const AuthPromptStatus = { }; /** @enum {number} */ @@ -1287,7 +1529,7 @@ index 2614b82410..ea09f470b4 100644 PROVIDE_USERNAME: 0, DONT_PROVIDE_USERNAME: 1, REUSE_USERNAME: 2, -@@ -751,28 +751,28 @@ export const AuthPrompt = GObject.registerClass({ +@@ -752,28 +752,28 @@ export const AuthPrompt = GObject.registerClass({ else if (oldStatus === AuthPromptStatus.VERIFICATION_CANCELLED) this.emit('cancelled'); @@ -1323,7 +1565,7 @@ index 2614b82410..ea09f470b4 100644 addCharacter(unichar) { diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js -index f75497065c..f38de0c914 100644 +index f75497065..f38de0c91 100644 --- a/js/gdm/loginDialog.js +++ b/js/gdm/loginDialog.js @@ -1066,7 +1066,7 @@ export const LoginDialog = GObject.registerClass({ @@ -1350,10 +1592,10 @@ index f75497065c..f38de0c914 100644 this._showUserList(); else diff --git a/js/ui/unlockDialog.js b/js/ui/unlockDialog.js -index 7f4082b92a..c52abc4d7e 100644 +index 85e469fb8..980353fe1 100644 --- a/js/ui/unlockDialog.js +++ b/js/ui/unlockDialog.js -@@ -846,9 +846,9 @@ export const UnlockDialog = GObject.registerClass({ +@@ -838,9 +838,9 @@ export const UnlockDialog = GObject.registerClass({ this.emit('failed'); } @@ -1366,27 +1608,27 @@ index 7f4082b92a..c52abc4d7e 100644 userName = this._userName; } else { -- -2.53.0 +2.54.0 -From f0da5c72c6ee44eee36a572aa8f97ce48f742480 Mon Sep 17 00:00:00 2001 +From cf3bb980c1f312897a9f42e9ef578d908a9ef0ab Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Wed, 11 Feb 2026 16:11:59 +0100 -Subject: [PATCH 17/42] unlockDialog: Use isprint instead of isgraph for +Subject: [PATCH 25/54] 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 +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. --- 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 c52abc4d7e..a648243a24 100644 +index 980353fe1..b0f51e533 100644 --- a/js/ui/unlockDialog.js +++ b/js/ui/unlockDialog.js -@@ -686,7 +686,7 @@ export const UnlockDialog = GObject.registerClass({ +@@ -678,7 +678,7 @@ export const UnlockDialog = GObject.registerClass({ this._showPrompt(); @@ -1396,77 +1638,59 @@ index c52abc4d7e..a648243a24 100644 return Clutter.EVENT_PROPAGATE; -- -2.53.0 +2.54.0 -From 30b8c4d9febced8814d37c9f55835950a438d2ac Mon Sep 17 00:00:00 2001 +From 5fa48f56386c9e8255c6767a7c2864fc51ffdf1b Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Wed, 21 Jan 2026 14:34:11 +0100 -Subject: [PATCH 18/42] authPrompt: Capture preemptive input before entry is +Subject: [PATCH 26/54] 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. Also, remove unsused addCharacter() in loginDialog.js. --- - js/gdm/authPrompt.js | 39 +++++++++++++++++++++++++++++++-------- + js/gdm/authPrompt.js | 26 ++++++++++++++++++-------- js/gdm/loginDialog.js | 4 ---- js/ui/unlockDialog.js | 2 +- - 3 files changed, 32 insertions(+), 13 deletions(-) + 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index ea09f470b4..5a09c3063b 100644 +index 7f3752d82..137361d24 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -155,6 +155,19 @@ export const AuthPrompt = GObject.registerClass({ - this.cancel(); - return Clutter.EVENT_STOP; - } +@@ -593,12 +593,22 @@ export const AuthPrompt = GObject.registerClass({ + opacity: 0, + visible: true, + }); +- this.updateSensitivity({sensitive: false}); ++ // Don't disable sensitivity on preemptive input ++ if (!this._preemptiveInput) ++ this.updateSensitivity({sensitive: false}); + -+ if (this._preemptiveInput && !this._pendingActivate) { -+ const unichar = event.get_key_unicode(); -+ if (event.get_key_symbol() === 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 Clutter.EVENT_PROPAGATE; - } - -@@ -593,10 +606,20 @@ export const AuthPrompt = GObject.registerClass({ + element.ease({ opacity: 255, duration: MESSAGE_FADE_OUT_ANIMATION_TIME, transition: Clutter.AnimationMode.EASE_OUT_QUAD, - onComplete: () => this.updateSensitivity({sensitive: true}), + onComplete: () => { + this.updateSensitivity({sensitive: true}); -+ this._completePreemptiveInput(element); ++ this._preemptiveInput = false; ++ }, ++ onStopped: isFinished => { ++ if (!isFinished) ++ this._preemptiveInput = false; + }, }); } -+ _completePreemptiveInput(element) { -+ if (element === this._entryArea && this._pendingActivate) -+ this._activateNext(); -+ this._preemptiveInput = false; -+ this._pendingActivate = false; -+ } -+ - setChoiceList(promptMessage, choiceList) { - this._authList.clear(); - this._authList.label.text = promptMessage; -@@ -719,7 +742,7 @@ export const AuthPrompt = GObject.registerClass({ +@@ -721,7 +731,7 @@ export const AuthPrompt = GObject.registerClass({ } reset(params) { @@ -1475,7 +1699,7 @@ index ea09f470b4..5a09c3063b 100644 reuseEntryText: false, softReset: false, }); -@@ -739,6 +762,8 @@ export const AuthPrompt = GObject.registerClass({ +@@ -741,6 +751,8 @@ export const AuthPrompt = GObject.registerClass({ if (this._userVerifier) this._userVerifier.cancel(); @@ -1484,7 +1708,7 @@ index ea09f470b4..5a09c3063b 100644 this._queryingService = null; this.clear({reuseEntryText}); this._message.opacity = 0; -@@ -775,12 +800,10 @@ export const AuthPrompt = GObject.registerClass({ +@@ -776,11 +788,9 @@ export const AuthPrompt = GObject.registerClass({ this.emit('reset', resetType); } @@ -1493,16 +1717,14 @@ index ea09f470b4..5a09c3063b 100644 - return; - - this._entry.grab_key_focus(); -- this._entry.clutter_text.insert_unichar(unichar); + startPreemptiveInput(unichar) { + this._preemptiveInput = true; -+ this._entry.clutter_text.insert_text(unichar, this._entry.clutter_text.cursor_position); -+ this.grab_key_focus(); ++ this.updateSensitivity({sensitive: true}); + this._entry.clutter_text.insert_unichar(unichar); } - begin(params) { diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js -index f38de0c914..09e8073c65 100644 +index f38de0c91..09e8073c6 100644 --- a/js/gdm/loginDialog.js +++ b/js/gdm/loginDialog.js @@ -1593,10 +1593,6 @@ export const LoginDialog = GObject.registerClass({ @@ -1517,10 +1739,10 @@ index f38de0c914..09e8073c65 100644 this._authPrompt.finish(onComplete); } diff --git a/js/ui/unlockDialog.js b/js/ui/unlockDialog.js -index a648243a24..fffc58e07c 100644 +index b0f51e533..100990acc 100644 --- a/js/ui/unlockDialog.js +++ b/js/ui/unlockDialog.js -@@ -687,7 +687,7 @@ export const UnlockDialog = GObject.registerClass({ +@@ -679,7 +679,7 @@ export const UnlockDialog = GObject.registerClass({ this._showPrompt(); if (GLib.unichar_isprint(unichar)) @@ -1530,46 +1752,57 @@ index a648243a24..fffc58e07c 100644 return Clutter.EVENT_PROPAGATE; } -- -2.53.0 +2.54.0 -From 7a086c8fb3c08377c554e96202c0e183a5968646 Mon Sep 17 00:00:00 2001 +From f1228badd7cacfd5341b8be9ce7a51a461e45f16 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Tue, 10 Feb 2026 18:51:41 +0100 -Subject: [PATCH 19/42] authPrompt: On verificationFailed ensure input - sensitivity is disabled +Subject: [PATCH 27/54] 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. --- - js/gdm/authPrompt.js | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) + js/gdm/authPrompt.js | 7 +++++-- + 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index 5a09c3063b..a383208ecc 100644 +index 137361d24..2799094ea 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -470,7 +470,7 @@ export const AuthPrompt = GObject.registerClass({ +@@ -457,7 +457,10 @@ export const AuthPrompt = GObject.registerClass({ if (wasQueryingService) this._queryingService = null; - this.updateSensitivity({sensitive: canRetry}); -+ this.updateSensitivity({sensitive: false}); ++ if (canRetry) { ++ this._entry.text = ''; ++ this.updateSensitivity({sensitive: true}); ++ } this.stopSpinning(); if (!canRetry) +@@ -751,7 +754,7 @@ export const 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.53.0 +2.54.0 -From f4faa89270778bd0280c911cd5524498444205ef Mon Sep 17 00:00:00 2001 +From f6df2443529f8b0a976dc21043337c7f6f0f80a1 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Tue, 30 Sep 2025 17:43:22 +0200 -Subject: [PATCH 20/42] authPrompt: Update authList style +Subject: [PATCH 28/54] authPrompt: Update authList style Make the AuthListItem buttons a bit bigger and more rounded. @@ -1585,16 +1818,16 @@ Use accessible_name. Register classes with the new style. --- .../gnome-shell-sass/widgets/_login-lock.scss | 95 ++++++++-- - js/gdm/authList.js | 168 +++++++++++++++--- - js/gdm/authPrompt.js | 28 ++- + js/gdm/authList.js | 164 +++++++++++++++--- + js/gdm/authPrompt.js | 43 +++-- js/gdm/util.js | 6 +- - 4 files changed, 249 insertions(+), 48 deletions(-) + 4 files changed, 255 insertions(+), 53 deletions(-) diff --git a/data/theme/gnome-shell-sass/widgets/_login-lock.scss b/data/theme/gnome-shell-sass/widgets/_login-lock.scss -index 93dbe617b7..5a31725b1d 100644 +index 7024cc58d..f498d1f5d 100644 --- a/data/theme/gnome-shell-sass/widgets/_login-lock.scss +++ b/data/theme/gnome-shell-sass/widgets/_login-lock.scss -@@ -141,42 +141,107 @@ $_gdm_dialog_width: 25em; +@@ -149,42 +149,107 @@ $_gdm_dialog_width: 25em; // Authentication methods list .login-dialog-auth-list-view { -st-vfade-offset: 3em; @@ -1631,8 +1864,8 @@ index 93dbe617b7..5a31725b1d 100644 + .login-dialog-auth-list-item { + min-height: 3em; + padding: $base_padding * 1.5; -+ margin: 0 $base_margin * 5; + margin-bottom: $base_margin; ++ margin-right: $base_margin; } } @@ -1642,8 +1875,8 @@ index 93dbe617b7..5a31725b1d 100644 + border-radius: $base_border_radius * 1.5; + min-height: 3em; + padding: $base_padding * 1.5; -+ margin: 0 $base_margin * 5; + margin-bottom: $base_margin; ++ margin-right: $base_margin; + } - border-radius: $modal_radius * 0.6; @@ -1670,7 +1903,7 @@ index 93dbe617b7..5a31725b1d 100644 +.login-dialog-auth-list-item-subtitle { + @extend %heading; + text-align: center; -+ padding: $base_padding * 0.3 0; ++ padding: $base_padding * 0.3 2em; +} + +.login-dialog-auth-list-item-title { @@ -1718,15 +1951,16 @@ index 93dbe617b7..5a31725b1d 100644 // User list .login-dialog-user-list-view { diff --git a/js/gdm/authList.js b/js/gdm/authList.js -index 4873a05c58..3ce30efc29 100644 +index 4873a05c5..ae90f9874 100644 --- a/js/gdm/authList.js +++ b/js/gdm/authList.js -@@ -18,30 +18,141 @@ +@@ -18,30 +18,140 @@ import Clutter from 'gi://Clutter'; import GLib from 'gi://GLib'; import GObject from 'gi://GObject'; +import Graphene from 'gi://Graphene'; import Meta from 'gi://Meta'; ++import Pango from 'gi://Pango'; +import Shell from 'gi://Shell'; import St from 'gi://St'; @@ -1762,7 +1996,7 @@ index 4873a05c58..3ce30efc29 100644 + const item = new PopupMenu.PopupBaseMenuItem({ + reactive: false, + can_focus: false, -+ }); + }); + item.add_child(labels); + this.addMenuItem(item); + @@ -1773,14 +2007,12 @@ index 4873a05c58..3ce30efc29 100644 + + this._menuManager = new PopupMenu.PopupMenuManager(sourceActor, { + actionMode: Shell.ActionMode.NONE, - }); ++ }); + this._menuManager.addMenu(this); + } +}; + +class ItemIcon extends St.Button { -+ static [GObject.GTypeName] = 'ItemIcon'; -+ + static { + GObject.registerClass(this); + } @@ -1788,7 +2020,7 @@ index 4873a05c58..3ce30efc29 100644 + constructor(iconName, iconTitle, iconSubtitle) { + super({ + style_class: 'login-dialog-item-icon', -+ child: new St.Icon({icon_name: iconName}), ++ iconName, + }); + + this._popup = new ItemIconPopup(this, iconTitle, iconSubtitle); @@ -1797,8 +2029,6 @@ index 4873a05c58..3ce30efc29 100644 +} + +class AuthListItem extends St.Button { -+ static [GObject.GTypeName] = 'AuthListItem'; -+ + static [GObject.signals] = { + 'activate': {}, + }; @@ -1842,6 +2072,7 @@ index 4873a05c58..3ce30efc29 100644 + y_align: Clutter.ActorAlign.CENTER, + x_expand: true, + }); ++ label.clutter_text.ellipsize = Pango.EllipsizeMode.END; + this._labelBox.add_child(label); + } + @@ -1852,6 +2083,7 @@ index 4873a05c58..3ce30efc29 100644 + y_align: Clutter.ActorAlign.CENTER, + x_expand: true, + }); ++ label.clutter_text.ellipsize = Pango.EllipsizeMode.END; + this._labelBox.add_child(label); + } + @@ -1875,7 +2107,7 @@ index 4873a05c58..3ce30efc29 100644 this.connect('key-focus-in', () => this._setSelected(true)); -@@ -65,25 +176,29 @@ const AuthListItem = GObject.registerClass({ +@@ -65,25 +175,26 @@ const AuthListItem = GObject.registerClass({ this.remove_style_pseudo_class('selected'); } } @@ -1885,8 +2117,6 @@ index 4873a05c58..3ce30efc29 100644 -export const AuthList = GObject.registerClass({ - Signals: { +export class AuthList extends St.BoxLayout { -+ static [GObject.GTypeName] = 'AuthList'; -+ + static [GObject.signals] = { 'activate': {param_types: [GObject.TYPE_STRING]}, 'item-added': {param_types: [AuthListItem.$gtype]}, @@ -1905,9 +2135,8 @@ index 4873a05c58..3ce30efc29 100644 orientation: Clutter.Orientation.VERTICAL, style_class: 'login-dialog-auth-list-layout', - x_align: Clutter.ActorAlign.START, -+ x_align: Clutter.ActorAlign.FILL, y_align: Clutter.ActorAlign.CENTER, -+ x_expand: true, ++ x_align: Clutter.ActorAlign.CENTER, }); - this.label = new St.Label({style_class: 'login-dialog-auth-list-title'}); @@ -1916,7 +2145,7 @@ index 4873a05c58..3ce30efc29 100644 this._box = new St.BoxLayout({ orientation: Clutter.Orientation.VERTICAL, style_class: 'login-dialog-auth-list', -@@ -136,10 +251,10 @@ export const AuthList = GObject.registerClass({ +@@ -136,10 +247,10 @@ export const AuthList = GObject.registerClass({ }); } @@ -1929,7 +2158,7 @@ index 4873a05c58..3ce30efc29 100644 this._box.add_child(item); this._items.set(key, item); -@@ -170,8 +285,7 @@ export const AuthList = GObject.registerClass({ +@@ -170,8 +281,7 @@ export const AuthList = GObject.registerClass({ } clear() { @@ -1940,10 +2169,23 @@ index 4873a05c58..3ce30efc29 100644 -}); +} diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index a383208ecc..8d7dfb9bae 100644 +index 2799094ea..de53ced39 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -216,13 +216,30 @@ export const AuthPrompt = GObject.registerClass({ +@@ -180,11 +180,7 @@ export const AuthPrompt = GObject.registerClass({ + this._mainBox.add_child(this.cancelButton); + + this._authList = new AuthList.AuthList(); +- this._authList.set({ +- visible: false, +- x_align: Clutter.ActorAlign.FILL, +- x_expand: true, +- }); ++ this._authList.hide(); + this._authList.connect('activate', (list, key) => { + this._authList.reactive = false; + this._authList.ease({ +@@ -192,13 +188,39 @@ export const AuthPrompt = GObject.registerClass({ duration: MESSAGE_FADE_OUT_ANIMATION_TIME, mode: Clutter.AnimationMode.EASE_OUT_QUAD, onComplete: () => { @@ -1955,8 +2197,10 @@ index a383208ecc..8d7dfb9bae 100644 }); }); - this._mainBox.add_child(this._authList); -+ this.add_child(this._authList); ++ this._inputWell.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({ + style_class: 'login-dialog-auth-list-title', + x_expand: true, @@ -1964,26 +2208,33 @@ index a383208ecc..8d7dfb9bae 100644 + 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); ++ ++ // Use bind_property instead of BindConstraint because ++ // BoxLayout allocation overrides constraint-set width. ++ this._authListTitle.bind_property('width', ++ this._authList, 'width', ++ GObject.BindingFlags.DEFAULT); this._entryArea = new St.Widget({ style_class: 'login-dialog-prompt-entry-area', -@@ -575,6 +592,7 @@ export const AuthPrompt = GObject.registerClass({ +@@ -567,6 +589,7 @@ export const AuthPrompt = GObject.registerClass({ + this.stopSpinning(); + } - this._entryArea.hide(); - this.stopSpinning(); + this._authListTitle.child.text = ''; this._authList.clear(); this._authList.hide(); -@@ -622,10 +640,10 @@ export const AuthPrompt = GObject.registerClass({ +@@ -617,10 +640,10 @@ export const AuthPrompt = GObject.registerClass({ setChoiceList(promptMessage, choiceList) { this._authList.clear(); @@ -1999,7 +2250,7 @@ index a383208ecc..8d7dfb9bae 100644 this._entryArea.hide(); diff --git a/js/gdm/util.js b/js/gdm/util.js -index 4b0f763f0a..b71251458e 100644 +index 4b0f763f0..b71251458 100644 --- a/js/gdm/util.js +++ b/js/gdm/util.js @@ -723,7 +723,11 @@ export class ShellUserVerifier extends Signals.EventEmitter { @@ -2016,13 +2267,13 @@ index 4b0f763f0a..b71251458e 100644 _onInfo(client, serviceName, info) { -- -2.53.0 +2.54.0 -From 199fec44007815189b8b23fad04b199a40982768 Mon Sep 17 00:00:00 2001 +From 3ee1292db420b3314755a6ee4c67169d38c1d35f Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Wed, 8 Oct 2025 18:48:07 +0200 -Subject: [PATCH 21/42] authPrompt: Let back button go back to step 1 instead +Subject: [PATCH 29/54] 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 @@ -2032,14 +2283,14 @@ go back to step 1 instead of full reset. Show backButton in unlockDialog for these cases. --- - js/gdm/authPrompt.js | 38 ++++++++++++++++++++++++++++---------- - 1 file changed, 28 insertions(+), 10 deletions(-) + js/gdm/authPrompt.js | 36 ++++++++++++++++++++++++++++-------- + 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index 8d7dfb9bae..3326096a2d 100644 +index de53ced39..e283adfb7 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -70,6 +70,7 @@ export const AuthPrompt = GObject.registerClass({ +@@ -69,6 +69,7 @@ export const AuthPrompt = GObject.registerClass({ this._mode = mode; this._defaultButtonWellActor = null; this._cancelledRetries = 0; @@ -2047,16 +2298,7 @@ index 8d7dfb9bae..3326096a2d 100644 this._idleMonitor = global.backend.get_core_idle_monitor(); -@@ -101,8 +102,6 @@ export const AuthPrompt = GObject.registerClass({ - }); - this.add_child(this._userWell); - -- this._hasCancelButton = this._mode === AuthPromptMode.UNLOCK_OR_LOG_IN; -- - this._initInputRow(); - - let capsLockPlaceholder = new St.Label(); -@@ -184,8 +183,8 @@ export const AuthPrompt = GObject.registerClass({ +@@ -167,16 +168,14 @@ export const AuthPrompt = GObject.registerClass({ style_class: 'login-dialog-button cancel-button', accessible_name: _('Cancel'), button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE, @@ -2064,13 +2306,10 @@ index 8d7dfb9bae..3326096a2d 100644 - can_focus: this._hasCancelButton, + reactive: true, + can_focus: true, - x_expand: true, x_align: Clutter.ActorAlign.START, y_align: Clutter.ActorAlign.CENTER, -@@ -197,10 +196,8 @@ export const AuthPrompt = GObject.registerClass({ - pivot_point: new Graphene.Point({x: 1, y: 0}), - })); - + icon_name: 'go-previous-symbolic', + }); - if (this._hasCancelButton) - this.cancelButton.connect('clicked', () => this.cancel()); - else @@ -2080,8 +2319,8 @@ index 8d7dfb9bae..3326096a2d 100644 this._mainBox.add_child(this.cancelButton); this._authList = new AuthList.AuthList(); -@@ -311,6 +308,16 @@ export const AuthPrompt = GObject.registerClass({ - this.setActorInDefaultButtonWell(this._nextButton); +@@ -303,6 +302,16 @@ export const AuthPrompt = GObject.registerClass({ + })); } + _updateCancelButton() { @@ -2089,7 +2328,7 @@ index 8d7dfb9bae..3326096a2d 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; + } @@ -2097,7 +2336,7 @@ index 8d7dfb9bae..3326096a2d 100644 showTimedLoginIndicator(time) { let hold = new Batch.Hold(); -@@ -397,6 +404,9 @@ export const AuthPrompt = GObject.registerClass({ +@@ -389,6 +398,9 @@ export const AuthPrompt = GObject.registerClass({ this.clear(); this._queryingService = serviceName; @@ -2107,7 +2346,7 @@ index 8d7dfb9bae..3326096a2d 100644 if (this._preemptiveAnswer) { this._userVerifier.answerQuery(this._queryingService, this._preemptiveAnswer); this._preemptiveAnswer = null; -@@ -421,6 +431,8 @@ export const AuthPrompt = GObject.registerClass({ +@@ -413,6 +425,8 @@ export const AuthPrompt = GObject.registerClass({ this.clear(); this._queryingService = serviceName; @@ -2116,7 +2355,7 @@ index 8d7dfb9bae..3326096a2d 100644 if (this._preemptiveAnswer) this._preemptiveAnswer = null; -@@ -767,10 +779,10 @@ export const AuthPrompt = GObject.registerClass({ +@@ -764,10 +778,10 @@ export const AuthPrompt = GObject.registerClass({ const oldStatus = this.verificationStatus; this.verificationStatus = AuthPromptStatus.NOT_VERIFYING; @@ -2129,7 +2368,7 @@ index 8d7dfb9bae..3326096a2d 100644 if (this._preemptiveAnswerWatchId) this._idleMonitor.remove_watch(this._preemptiveAnswerWatchId); -@@ -858,6 +870,12 @@ export const AuthPrompt = GObject.registerClass({ +@@ -854,6 +868,12 @@ export const AuthPrompt = GObject.registerClass({ if (this.verificationStatus === AuthPromptStatus.VERIFICATION_SUCCEEDED) return; @@ -2143,13 +2382,13 @@ index 8d7dfb9bae..3326096a2d 100644 this._cancelledRetries++; if (this._cancelledRetries > this._userVerifier.allowedFailures) -- -2.53.0 +2.54.0 -From 09d762ac05a793f5df5fd51be2e674fd0929252d Mon Sep 17 00:00:00 2001 +From 18ec89f28691df35659781d1c0abf9dfb1a303fd Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Mon, 20 Oct 2025 17:24:40 +0200 -Subject: [PATCH 22/42] loginDialog: Vertically center authPrompt using fixed +Subject: [PATCH 30/54] loginDialog: Vertically center authPrompt using fixed height Use a fixed estimated height for centering so the position stays @@ -2160,7 +2399,7 @@ interaction. 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js -index 09e8073c65..3a22bf161b 100644 +index 09e8073c6..3a22bf161 100644 --- a/js/gdm/loginDialog.js +++ b/js/gdm/loginDialog.js @@ -45,6 +45,7 @@ import * as A11y from '../ui/status/accessibility.js'; @@ -2210,13 +2449,13 @@ index 09e8073c65..3a22bf161b 100644 } -- -2.53.0 +2.54.0 -From cfec9c4e34f698ad5693d5e81daf1d31a8972ccb Mon Sep 17 00:00:00 2001 +From f086439b2e40afbad7c04fd2e3d3105b095d6c5f Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Tue, 12 Nov 2024 14:26:30 -0500 -Subject: [PATCH 23/42] data: Add fingerprint and vcard icons +Subject: [PATCH 31/54] data: Add fingerprint and vcard icons Fingerprint icon will be used to inform when it's being run un the background. @@ -2232,7 +2471,7 @@ have an Organization field in its subject. create mode 100644 data/icons/scalable/status/vcard-symbolic.svg diff --git a/data/gnome-shell-icons.gresource.xml b/data/gnome-shell-icons.gresource.xml -index 4c9a3446b5..11a5afa3fd 100644 +index 4c9a3446b..11a5afa3f 100644 --- a/data/gnome-shell-icons.gresource.xml +++ b/data/gnome-shell-icons.gresource.xml @@ -51,6 +51,7 @@ @@ -2253,7 +2492,7 @@ index 4c9a3446b5..11a5afa3fd 100644 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 @@ @@ -2287,7 +2526,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 @@ @@ -2301,56 +2540,54 @@ index 0000000000..1694f23645 + + -- -2.53.0 +2.54.0 -From 4df4492dfd42af8f9449249ad2f5778b63e9f4d7 Mon Sep 17 00:00:00 2001 +From 2df9a17d2e55f832b5041feca42e8859cf804f26 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Mon, 18 Aug 2025 12:30:50 +0200 -Subject: [PATCH 24/42] gdm: Extract authentication service and role constants - to const.js +Subject: [PATCH 32/54] gdm: Extract authentication service and role constants + to constants.js Create a dedicated const.js module to centralize GDM authentication-related constants that are shared across multiple files. -Also, in utils, add isSelectable() and getIconName() to get mechanisms metadata. - This prepares the codebase for upcoming commits that will use these constants. --- - js/gdm/authPrompt.js | 3 +- - js/gdm/const.js | 10 ++++++ - js/gdm/util.js | 64 +++++++++++++++++++++++++---------- + js/gdm/authPrompt.js | 3 ++- + js/gdm/constants.js | 10 ++++++++++ + js/gdm/util.js | 34 ++++++++++++++++------------------ js/js-resources.gresource.xml | 1 + - 4 files changed, 59 insertions(+), 19 deletions(-) - create mode 100644 js/gdm/const.js + 4 files changed, 29 insertions(+), 19 deletions(-) + create mode 100644 js/gdm/constants.js diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index 3326096a2d..7b3aa8558f 100644 +index e283adfb7..a5b7132a3 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -10,6 +10,7 @@ import St from 'gi://St'; +@@ -9,6 +9,7 @@ import St from 'gi://St'; import * as Animation from '../ui/animation.js'; import * as AuthList from './authList.js'; import * as Batch from './batch.js'; -+import * as Const from './const.js'; ++import * as Constants from './constants.js'; import * as GdmUtil from './util.js'; import * as Params from '../misc/params.js'; import * as ShellEntry from '../ui/shellEntry.js'; -@@ -457,7 +458,7 @@ export const AuthPrompt = GObject.registerClass({ +@@ -451,7 +452,7 @@ export const AuthPrompt = GObject.registerClass({ // with a smartcard // 2) Don't reset if we've already succeeded at verification and // the user is getting logged in. - if (this._userVerifier.serviceIsDefault(GdmUtil.SMARTCARD_SERVICE_NAME) && -+ if (this._userVerifier.serviceIsDefault(Const.SMARTCARD_SERVICE_NAME) && ++ if (this._userVerifier.serviceIsDefault(Constants.SMARTCARD_SERVICE_NAME) && (this.verificationStatus === AuthPromptStatus.VERIFYING || this.verificationStatus === AuthPromptStatus.VERIFICATION_IN_PROGRESS) && this.smartcardDetected) -diff --git a/js/gdm/const.js b/js/gdm/const.js +diff --git a/js/gdm/constants.js b/js/gdm/constants.js new file mode 100644 -index 0000000000..2f37446c8a +index 000000000..2f37446c8 --- /dev/null -+++ b/js/gdm/const.js ++++ b/js/gdm/constants.js @@ -0,0 +1,10 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + @@ -2363,10 +2600,18 @@ 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 b71251458e..660c6e4ddc 100644 +index b71251458..0c8ce34d3 100644 --- a/js/gdm/util.js +++ b/js/gdm/util.js -@@ -24,9 +24,6 @@ Gio._promisify(Gdm.UserVerifierProxy.prototype, +@@ -5,6 +5,7 @@ import GLib from 'gi://GLib'; + import * as Signals from '../misc/signals.js'; + + import * as Batch from './batch.js'; ++import * as Constants from './constants.js'; + import * as OVirt from './oVirt.js'; + import * as Vmware from './vmware.js'; + import * as Main from '../ui/main.js'; +@@ -24,9 +25,6 @@ Gio._promisify(Gdm.UserVerifierProxy.prototype, 'call_begin_verification_for_user'); Gio._promisify(Gdm.UserVerifierProxy.prototype, 'call_begin_verification'); @@ -2376,7 +2621,145 @@ index b71251458e..660c6e4ddc 100644 const CLONE_FADE_ANIMATION_TIME = 250; export const LOGIN_SCREEN_SCHEMA = 'org.gnome.login-screen'; -@@ -101,6 +98,37 @@ export function cloneAndFadeOutActor(actor) { +@@ -431,7 +429,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { + this._updateDefaultService(); + + if (this._userVerifier && +- !this._activeServices.has(FINGERPRINT_SERVICE_NAME)) { ++ !this._activeServices.has(Constants.FINGERPRINT_SERVICE_NAME)) { + if (!this._hold?.isAcquired()) + this._hold = new Batch.Hold(); + await this._maybeStartFingerprintVerification(); +@@ -485,8 +483,8 @@ export class ShellUserVerifier extends Signals.EventEmitter { + this.smartcardDetected = smartcardDetected; + + if (this.smartcardDetected) +- this._preemptingService = SMARTCARD_SERVICE_NAME; +- else if (this._preemptingService === SMARTCARD_SERVICE_NAME) ++ this._preemptingService = Constants.SMARTCARD_SERVICE_NAME; ++ else if (this._preemptingService === Constants.SMARTCARD_SERVICE_NAME) + this._preemptingService = null; + + this._updateDefaultService(); +@@ -605,7 +603,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { + return true; + } + +- return this.serviceIsForeground(SMARTCARD_SERVICE_NAME); ++ return this.serviceIsForeground(Constants.SMARTCARD_SERVICE_NAME); + } + + serviceIsDefault(serviceName) { +@@ -614,7 +612,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { + + serviceIsFingerprint(serviceName) { + return this._fingerprintReaderType !== FingerprintReaderType.NONE && +- serviceName === FINGERPRINT_SERVICE_NAME; ++ serviceName === Constants.FINGERPRINT_SERVICE_NAME; + } + + _onSettingsChanged() { +@@ -631,7 +629,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { + this._fingerprintManager = null; + this._fingerprintReaderType = FingerprintReaderType.NONE; + +- if (this._activeServices.has(FINGERPRINT_SERVICE_NAME)) ++ if (this._activeServices.has(Constants.FINGERPRINT_SERVICE_NAME)) + needsReset = true; + } + +@@ -641,7 +639,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { + this._smartcardManager.disconnectObject(this); + this._smartcardManager = null; + +- if (this._activeServices.has(SMARTCARD_SERVICE_NAME)) ++ if (this._activeServices.has(Constants.SMARTCARD_SERVICE_NAME)) + needsReset = true; + } + +@@ -651,13 +649,13 @@ export class ShellUserVerifier extends Signals.EventEmitter { + + _getDetectedDefaultService() { + if (this._smartcardManager?.loggedInWithToken()) +- return SMARTCARD_SERVICE_NAME; ++ return Constants.SMARTCARD_SERVICE_NAME; + else if (this._settings.get_boolean(PASSWORD_AUTHENTICATION_KEY)) +- return PASSWORD_SERVICE_NAME; ++ return Constants.PASSWORD_SERVICE_NAME; + else if (this._smartcardManager) +- return SMARTCARD_SERVICE_NAME; ++ return Constants.SMARTCARD_SERVICE_NAME; + else if (this._fingerprintReaderType !== FingerprintReaderType.NONE) +- return FINGERPRINT_SERVICE_NAME; ++ return Constants.FINGERPRINT_SERVICE_NAME; + return null; + } + +@@ -667,7 +665,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { + + if (!this._defaultService) { + log('no authentication service is enabled, using password authentication'); +- this._defaultService = PASSWORD_SERVICE_NAME; ++ this._defaultService = Constants.PASSWORD_SERVICE_NAME; + } + + if (oldDefaultService && +@@ -715,8 +713,8 @@ export class ShellUserVerifier extends Signals.EventEmitter { + async _maybeStartFingerprintVerification() { + if (this._userName && + this._fingerprintReaderType !== FingerprintReaderType.NONE && +- !this.serviceIsForeground(FINGERPRINT_SERVICE_NAME)) +- await this._startService(FINGERPRINT_SERVICE_NAME); ++ !this.serviceIsForeground(Constants.FINGERPRINT_SERVICE_NAME)) ++ await this._startService(Constants.FINGERPRINT_SERVICE_NAME); + } + + _onChoiceListQuery(client, serviceName, promptMessage, list) { +@@ -848,7 +846,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { + } + + async _verificationFailed(serviceName, shouldRetry) { +- if (serviceName === FINGERPRINT_SERVICE_NAME) { ++ if (serviceName === Constants.FINGERPRINT_SERVICE_NAME) { + if (this._fingerprintFailedId) + GLib.source_remove(this._fingerprintFailedId); + } +diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml +index e5e6167f1..041365e5d 100644 +--- a/js/js-resources.gresource.xml ++++ b/js/js-resources.gresource.xml +@@ -4,6 +4,7 @@ + gdm/authList.js + gdm/authPrompt.js + gdm/batch.js ++ gdm/constants.js + gdm/credentialManager.js + gdm/loginDialog.js + gdm/oVirt.js +-- +2.54.0 + + +From c15539a8f8e194f3d7dfc7dc9d9b1d6bce603340 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Wed, 11 Mar 2026 11:41:40 +0100 +Subject: [PATCH 33/54] gdm/util: Add helper functions to get future mechanisms + metadata + +1. isSelectable() to detect when a mechanism is selectable or runs in + background. +2. getNonSelectableIconName() to get the icon name for a non selectable mechanism + +Next commits will use these functions. +--- + js/gdm/util.js | 34 ++++++++++++++++++++++++++++++++++ + 1 file changed, 34 insertions(+) + +diff --git a/js/gdm/util.js b/js/gdm/util.js +index 0c8ce34d3..f0b99a0ff 100644 +--- a/js/gdm/util.js ++++ b/js/gdm/util.js +@@ -99,6 +99,40 @@ export function cloneAndFadeOutActor(actor) { return hold; } @@ -2386,10 +2769,10 @@ index b71251458e..660c6e4ddc 100644 + */ +export function isSelectable(mechanism) { + switch (mechanism.role) { -+ case Const.PASSWORD_ROLE_NAME: -+ case Const.SMARTCARD_ROLE_NAME: ++ case Constants.PASSWORD_ROLE_NAME: ++ case Constants.SMARTCARD_ROLE_NAME: + return true; -+ case Const.FINGERPRINT_ROLE_NAME: ++ case Constants.FINGERPRINT_ROLE_NAME: + return false; + default: + throw new Error(`Failed checking mechanism is selectable: ${mechanism.role}`); @@ -2400,11 +2783,14 @@ index b71251458e..660c6e4ddc 100644 + * @param {object} mechanism + * @returns {string} + */ -+export function getIconName(mechanism) { ++export function getNonSelectableIconName(mechanism) { + // This is only used for non selectable mechanisms. + // Currently only fingerprint is non selectable ++ if (isSelectable(mechanism)) ++ throw new Error(`Failed getting mechanism icon: ${mechanism.role}, is selectable`); ++ + switch (mechanism.role) { -+ case Const.FINGERPRINT_ROLE_NAME: ++ case Constants.FINGERPRINT_ROLE_NAME: + return 'fingerprint-auth-symbolic'; + default: + throw new Error(`Failed getting mechanism icon: ${mechanism.role}`); @@ -2414,129 +2800,14 @@ index b71251458e..660c6e4ddc 100644 export class ShellUserVerifier extends Signals.EventEmitter { constructor(client, params) { super(); -@@ -431,7 +459,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { - this._updateDefaultService(); - - if (this._userVerifier && -- !this._activeServices.has(FINGERPRINT_SERVICE_NAME)) { -+ !this._activeServices.has(Const.FINGERPRINT_SERVICE_NAME)) { - if (!this._hold?.isAcquired()) - this._hold = new Batch.Hold(); - await this._maybeStartFingerprintVerification(); -@@ -485,8 +513,8 @@ export class ShellUserVerifier extends Signals.EventEmitter { - this.smartcardDetected = smartcardDetected; - - if (this.smartcardDetected) -- this._preemptingService = SMARTCARD_SERVICE_NAME; -- else if (this._preemptingService === SMARTCARD_SERVICE_NAME) -+ this._preemptingService = Const.SMARTCARD_SERVICE_NAME; -+ else if (this._preemptingService === Const.SMARTCARD_SERVICE_NAME) - this._preemptingService = null; - - this._updateDefaultService(); -@@ -605,7 +633,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { - return true; - } - -- return this.serviceIsForeground(SMARTCARD_SERVICE_NAME); -+ return this.serviceIsForeground(Const.SMARTCARD_SERVICE_NAME); - } - - serviceIsDefault(serviceName) { -@@ -614,7 +642,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { - - serviceIsFingerprint(serviceName) { - return this._fingerprintReaderType !== FingerprintReaderType.NONE && -- serviceName === FINGERPRINT_SERVICE_NAME; -+ serviceName === Const.FINGERPRINT_SERVICE_NAME; - } - - _onSettingsChanged() { -@@ -631,7 +659,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { - this._fingerprintManager = null; - this._fingerprintReaderType = FingerprintReaderType.NONE; - -- if (this._activeServices.has(FINGERPRINT_SERVICE_NAME)) -+ if (this._activeServices.has(Const.FINGERPRINT_SERVICE_NAME)) - needsReset = true; - } - -@@ -641,7 +669,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { - this._smartcardManager.disconnectObject(this); - this._smartcardManager = null; - -- if (this._activeServices.has(SMARTCARD_SERVICE_NAME)) -+ if (this._activeServices.has(Const.SMARTCARD_SERVICE_NAME)) - needsReset = true; - } - -@@ -651,13 +679,13 @@ export class ShellUserVerifier extends Signals.EventEmitter { - - _getDetectedDefaultService() { - if (this._smartcardManager?.loggedInWithToken()) -- return SMARTCARD_SERVICE_NAME; -+ return Const.SMARTCARD_SERVICE_NAME; - else if (this._settings.get_boolean(PASSWORD_AUTHENTICATION_KEY)) -- return PASSWORD_SERVICE_NAME; -+ return Const.PASSWORD_SERVICE_NAME; - else if (this._smartcardManager) -- return SMARTCARD_SERVICE_NAME; -+ return Const.SMARTCARD_SERVICE_NAME; - else if (this._fingerprintReaderType !== FingerprintReaderType.NONE) -- return FINGERPRINT_SERVICE_NAME; -+ return Const.FINGERPRINT_SERVICE_NAME; - return null; - } - -@@ -667,7 +695,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { - - if (!this._defaultService) { - log('no authentication service is enabled, using password authentication'); -- this._defaultService = PASSWORD_SERVICE_NAME; -+ this._defaultService = Const.PASSWORD_SERVICE_NAME; - } - - if (oldDefaultService && -@@ -715,8 +743,8 @@ export class ShellUserVerifier extends Signals.EventEmitter { - async _maybeStartFingerprintVerification() { - if (this._userName && - this._fingerprintReaderType !== FingerprintReaderType.NONE && -- !this.serviceIsForeground(FINGERPRINT_SERVICE_NAME)) -- await this._startService(FINGERPRINT_SERVICE_NAME); -+ !this.serviceIsForeground(Const.FINGERPRINT_SERVICE_NAME)) -+ await this._startService(Const.FINGERPRINT_SERVICE_NAME); - } - - _onChoiceListQuery(client, serviceName, promptMessage, list) { -@@ -848,7 +876,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { - } - - async _verificationFailed(serviceName, shouldRetry) { -- if (serviceName === FINGERPRINT_SERVICE_NAME) { -+ if (serviceName === Const.FINGERPRINT_SERVICE_NAME) { - if (this._fingerprintFailedId) - GLib.source_remove(this._fingerprintFailedId); - } -diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml -index e5e6167f15..ee6b77d322 100644 ---- a/js/js-resources.gresource.xml -+++ b/js/js-resources.gresource.xml -@@ -4,6 +4,7 @@ - gdm/authList.js - gdm/authPrompt.js - gdm/batch.js -+ gdm/const.js - gdm/credentialManager.js - gdm/loginDialog.js - gdm/oVirt.js -- -2.53.0 +2.54.0 -From 90848a72fd7a322d83a7f0319d31bf6a498e01d1 Mon Sep 17 00:00:00 2001 +From 0fee132754e6a7cf9eb62324d0898988228cf696 Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Tue, 6 Feb 2024 11:04:20 -0500 -Subject: [PATCH 25/42] gdm: Add new AuthMenuButton control +Subject: [PATCH 34/54] 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. @@ -2555,18 +2826,17 @@ 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. --- - .../gnome-shell-sass/widgets/_login-lock.scss | 42 ++ - js/gdm/authMenuButton.js | 613 ++++++++++++++++++ + .../gnome-shell-sass/widgets/_login-lock.scss | 59 ++ + js/gdm/authMenuButton.js | 504 ++++++++++++++++++ js/js-resources.gresource.xml | 1 + - js/ui/popupMenu.js | 22 + - 4 files changed, 678 insertions(+) + 3 files changed, 564 insertions(+) create mode 100644 js/gdm/authMenuButton.js diff --git a/data/theme/gnome-shell-sass/widgets/_login-lock.scss b/data/theme/gnome-shell-sass/widgets/_login-lock.scss -index 5a31725b1d..45d47f0652 100644 +index f498d1f5d..61ec97499 100644 --- a/data/theme/gnome-shell-sass/widgets/_login-lock.scss +++ b/data/theme/gnome-shell-sass/widgets/_login-lock.scss -@@ -67,6 +67,48 @@ $_gdm_dialog_width: 25em; +@@ -75,6 +75,65 @@ $_gdm_dialog_width: 25em; } } @@ -2574,34 +2844,45 @@ index 5a31725b1d..45d47f0652 100644 + padding: $base_padding * 3; + margin-right: $base_padding * 2; + -+ .login-dialog-auth-menu-header { -+ @include fontsize($base_font_size - 1); -+ text-align: center; -+ font-weight: bold; -+ padding-top: $base_padding * 3; -+ padding-bottom: $base_padding; ++ .login-dialog-auth-menu-item-section { ++ padding-top: $base_padding; + + &:first-child { -+ padding-top: $base_padding; ++ padding-top: 0; ++ } ++ ++ &-label { ++ @include fontsize($base_font_size - 1); ++ font-weight: bold; ++ color: $_gdm_fg; + } + } + -+ .login-dialog-auth-menu-item-indicator { ++ .login-dialog-auth-menu-item-icon { ++ color: $_gdm_fg; ++ icon-size: $scalable_icon_size; ++ margin-right: $base_margin * 2; ++ } ++ ++ .login-dialog-auth-menu-item-box { + spacing: $base_padding * .5; + -+ .login-dialog-auth-menu-item-indicator-name { -+ @include fontsize($base_font_size + 1.75); ++ &-name { ++ color: $_gdm_fg; ++ @include fontsize($base_font_size); + font-weight: bold; + } + -+ .login-dialog-auth-menu-item-indicator-description { -+ @include fontsize($base_font_size - .25); ++ &-description { ++ color: $_gdm_fg; ++ @include fontsize($base_font_size - 1); + } + } +} + +.login-dialog-auth-menu-button-indicator { + background-color: transparent !important; ++ margin: 32px; + + .login-dialog-auth-menu-button-indicator-icons { + spacing: $base_padding * 3; @@ -2610,6 +2891,12 @@ index 5a31725b1d..45d47f0652 100644 + icon-size: 2em; + } + } ++ ++ .login-dialog-auth-menu-button-indicator-description { ++ @include fontsize($base_font_size); ++ font-weight: normal; ++ margin-left: $base_padding * 2; ++ } +} + .login-dialog-button-box { @@ -2617,10 +2904,10 @@ index 5a31725b1d..45d47f0652 100644 } diff --git a/js/gdm/authMenuButton.js b/js/gdm/authMenuButton.js new file mode 100644 -index 0000000000..b5018d8382 +index 000000000..cbc1d7676 --- /dev/null +++ b/js/gdm/authMenuButton.js -@@ -0,0 +1,613 @@ +@@ -0,0 +1,504 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- +/* + * Copyright 2024 Red Hat, Inc @@ -2672,81 +2959,78 @@ index 0000000000..b5018d8382 +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'; + +const VISIBILITY_ANIMATION_TIME = 200; + -+class AuthMenuItem extends PopupMenu.PopupImageMenuItem { -+ static [GObject.GTypeName] = 'AuthMenuItem'; ++class AuthMenuItemSection extends PopupMenu.PopupMenuSection { ++ constructor(sectionName) { ++ super(); + -+ static { -+ GObject.registerClass(this); -+ } ++ this.actor.add_style_class_name('login-dialog-auth-menu-item-section'); + -+ constructor(item, params) { -+ super(item.name, item.iconName || '', params); -+ -+ // Move ornament to the left -+ this.set_child_at_index(this._ornamentIcon, 0); -+ } -+ -+ updateLabelActor(labelActor) { -+ this.insert_child_below(labelActor, this.label_actor); -+ this.remove_child(this.label_actor); -+ -+ this.label_actor.destroy(); -+ this.label_actor = labelActor; -+ } -+} -+ -+class AuthMenuItemIndicator extends AuthMenuItem { -+ static [GObject.GTypeName] = 'AuthMenuItemIndicator'; -+ -+ static { -+ GObject.registerClass(this); -+ } -+ -+ constructor(item, params) { -+ super(item, params); -+ -+ if (item.description) { -+ const box = new St.BoxLayout({ -+ vertical: true, -+ style_class: 'login-dialog-auth-menu-item-indicator', ++ if (sectionName) { ++ const itemSection = new PopupMenu.PopupMenuItem(sectionName, { ++ reactive: false, ++ can_focus: false, + }); -+ -+ const nameLabel = new St.Label({ -+ text: item.name, -+ style_class: 'login-dialog-auth-menu-item-indicator-name', -+ }); -+ -+ const descriptionLabel = new St.Label({ -+ text: item.description, -+ style_class: 'login-dialog-auth-menu-item-indicator-description', -+ }); -+ -+ box.add_child(nameLabel); -+ box.add_child(descriptionLabel); -+ -+ this.updateLabelActor(box); ++ itemSection.label.style_class = 'login-dialog-auth-menu-item-section-label'; ++ itemSection.label.x_expand = true; ++ itemSection.label.x_align = Clutter.ActorAlign.CENTER; ++ this.addMenuItem(itemSection); + } + } +} + -+export class AuthMenuButton extends St.Bin { -+ static [GObject.GTypeName] = 'AuthMenuButton'; ++class AuthMenuItem extends PopupMenu.PopupBaseMenuItem { ++ static { ++ GObject.registerClass(this); ++ } + ++ constructor(item, params = {}) { ++ super(params); ++ ++ if (item.description && item.iconName) { ++ const icon = new St.Icon({ ++ style_class: 'login-dialog-auth-menu-item-icon', ++ icon_name: item.iconName, ++ x_align: Clutter.ActorAlign.CENTER, ++ }); ++ this.add_child(icon); ++ ++ const box = new St.BoxLayout({ ++ vertical: true, ++ style_class: 'login-dialog-auth-menu-item-box', ++ }); ++ const nameLabel = new St.Label({ ++ text: item.name, ++ style_class: 'login-dialog-auth-menu-item-box-name', ++ }); ++ const descriptionLabel = new St.Label({ ++ text: item.description, ++ style_class: 'login-dialog-auth-menu-item-box-description', ++ }); ++ box.add_child(nameLabel); ++ box.add_child(descriptionLabel); ++ this.add_child(box); ++ ++ this.label_actor = nameLabel; ++ } else { ++ // When no extra info is provided, use the default style ++ const nameLabel = new St.Label({ ++ text: item.name, ++ y_expand: true, ++ y_align: Clutter.ActorAlign.CENTER, ++ }); ++ this.add_child(nameLabel); ++ this.label_actor = nameLabel; ++ } ++ } ++} ++ ++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, @@ -2756,7 +3040,7 @@ index 0000000000..b5018d8382 + 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), + }; + @@ -2768,168 +3052,54 @@ index 0000000000..b5018d8382 + GObject.registerClass(this); + } + -+ constructor(params) { -+ params.sectionOrder ??= []; -+ super(params); -+ -+ const button = new St.Button({ ++ constructor(params = {}) { ++ super({ ++ icon_name: 'cog-wheel-symbolic', ++ sectionOrder: [], ++ ...params, + style_class: 'login-dialog-button login-dialog-auth-menu-button', -+ child: new St.Icon({icon_name: this.iconName}), -+ reactive: true, -+ track_hover: true, + can_focus: true, -+ accessible_name: this.title, + accessible_role: Atk.Role.MENU, -+ x_align: Clutter.ActorAlign.CENTER, -+ y_align: Clutter.ActorAlign.CENTER, + }); -+ -+ this.child = button; -+ -+ this._button = button; ++ 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'); + Main.uiGroup.add_child(this._menu.actor); + this._menu.actor.hide(); + -+ this._menu.connect('open-state-changed', (_menu, isOpen) => { -+ if (this.readOnly) { -+ if (isOpen) -+ this._addMenuShield(); -+ else -+ this._removeMenuShield(); -+ } -+ }); -+ -+ this._manager = new PopupMenu.PopupMenuManager(this._button, ++ this._manager = new PopupMenu.PopupMenuManager(this, + {actionMode: Shell.ActionMode.NONE}); + this._manager.addMenu(this._menu); + -+ this._button.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._headers = new Map(); -+ this.updateSensitivity(true); + } + -+ _addMenuShield() { -+ if (this._menuShield) -+ return; -+ -+ this._menuShield = new St.Widget({ -+ reactive: true, -+ opacity: 0, -+ }); -+ -+ Main.uiGroup.add_child(this._menuShield); -+ -+ this._menuShield.add_constraint(new Clutter.BindConstraint({ -+ source: this._menu.actor, -+ coordinate: Clutter.BindCoordinate.ALL, -+ })); -+ } -+ -+ _removeMenuShield() { -+ if (this._menuShield) { -+ this._menuShield.destroy(); -+ this._menuShield = null; -+ } -+ } -+ -+ _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._button.reactive = reactive; -+ this._button.can_focus = reactive; -+ } -+ -+ updateSensitivity(sensitive) { -+ this._sensitive = sensitive; -+ -+ const visibleItems = this._getVisibleItemsCount(); -+ if (visibleItems === 0 || (visibleItems <= 1 && !this.readOnly)) -+ sensitive = false; -+ -+ this._button.reactive = sensitive; -+ this._button.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); + } + } + -+ getSections() { -+ const sectionsSet = new Set(); -+ for (const itemKey of this._items.keys()) { -+ const item = JSON.parse(itemKey); -+ if (item.sectionName) -+ sectionsSet.add(item.sectionName); -+ } -+ -+ const sections = Array.from(sectionsSet); -+ sections.sort((a, b) => { -+ const indexA = this.sectionOrder.indexOf(a); -+ const indexB = this.sectionOrder.indexOf(b); -+ -+ if (indexA !== -1 && indexB !== -1) -+ return indexA - indexB; -+ if (indexA !== -1) -+ return -1; -+ if (indexB !== -1) -+ return 1; -+ return 0; -+ }); -+ -+ return sections; -+ } -+ + _deepEquals(a, b) { + if (a === b) + return true; @@ -2937,6 +3107,9 @@ index 0000000000..b5018d8382 + 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); + @@ -2961,9 +3134,6 @@ index 0000000000..b5018d8382 + + let criteriaMismatch = false; + for (const key of Object.keys(searchCriteria)) { -+ if (!searchCriteria[key]) -+ continue; -+ + if (this._deepEquals(item[key], searchCriteria[key])) + continue; + @@ -2985,8 +3155,6 @@ index 0000000000..b5018d8382 + } + + clearItems(searchCriteria = {}) { -+ const sections = this.getSections(); -+ + this._findItems(searchCriteria).forEach(itemKey => { + const menuItem = this._items.get(itemKey); + this._activeItems.delete(itemKey); @@ -2994,19 +3162,18 @@ index 0000000000..b5018d8382 + this._items.delete(itemKey); + }); + -+ sections.forEach(sectionName => { ++ this._sections.keys().forEach(sectionName => { + const itemsInSection = this._findItems({sectionName}); + if (itemsInSection.length === 0) { -+ const header = this._headers.get(sectionName); -+ if (header) { -+ header.destroy(); -+ this._headers.delete(sectionName); ++ const section = this._sections.get(sectionName); ++ if (section) { ++ section.destroy(); ++ this._sections.delete(sectionName); + } + } + }); + -+ this._updateVisibility(); -+ this.updateSensitivity(this._sensitive); ++ this.updateVisibility(); + } + + addItem(item) { @@ -3017,83 +3184,90 @@ index 0000000000..b5018d8382 + if (!item.name) + throw new Error(`item ${itemKey} lacks name`); + -+ const sectionName = item.sectionName ?? null; -+ if (sectionName && !this._headers.has(sectionName)) { -+ const header = new St.Label({ -+ text: sectionName, -+ style_class: 'login-dialog-auth-menu-header', -+ y_align: Clutter.ActorAlign.START, -+ y_expand: true, -+ }); -+ -+ let insertIndex = 0; -+ const orderIndex = this.sectionOrder.indexOf(sectionName); -+ -+ if (orderIndex === -1) { -+ insertIndex = -1; -+ } else { -+ for (const existingSectionName of this._headers.keys()) { -+ const existingIndex = this.sectionOrder.indexOf(existingSectionName); -+ if (existingIndex === -1 || existingIndex > orderIndex) -+ break; -+ insertIndex++; -+ } -+ } -+ -+ this._menu.box.insert_child_at_index(header, insertIndex); -+ this._headers.set(sectionName, header); -+ } -+ -+ const menuItem = this._createMenuItem(item); -+ menuItem.setOrnament(PopupMenu.Ornament.HIDDEN); -+ ++ const menuItem = new AuthMenuItem(item, {reactive: !this.readOnly}); + menuItem.connect('activate', () => { + this.setActiveItem(item); + }); + -+ if (sectionName) { -+ const children = this._menu.box.get_children(); -+ const header = this._headers.get(sectionName); -+ const headerIndex = children.indexOf(header); -+ -+ let insertIndex = headerIndex + 1; -+ while (insertIndex < children.length && children[insertIndex] instanceof AuthMenuItem) -+ insertIndex++; -+ -+ this._menu.box.insert_child_at_index(menuItem, insertIndex); -+ } else { -+ this._menu.box.add_child(menuItem); -+ } ++ const section = this._getSection(item.sectionName); ++ section.addMenuItem(menuItem); + + this._items.set(itemKey, menuItem); -+ this._updateVisibility(); -+ this.updateSensitivity(this._sensitive); ++ this.updateVisibility(); + } + -+ _createMenuItem(item) { -+ return new AuthMenuItem(item); -+ } ++ _getSection(sectionName) { ++ const key = sectionName ?? null; ++ let section = this._sections.get(key); ++ if (section) ++ return section; + -+ _updateVisibility() { -+ const visibleSections = this._getVisibleSections(); ++ section = new AuthMenuItemSection(sectionName); + -+ for (const [sectionName, header] of this._headers) { -+ const showThisSection = visibleSections.includes(sectionName); -+ -+ header.visible = showThisSection; -+ -+ const sectionItems = this._findItems({sectionName}); -+ for (const itemKey of sectionItems) { -+ const menuItem = this._items.get(itemKey); -+ menuItem.visible = showThisSection; ++ if (key === null) { ++ // Empty section is always last ++ this._menu.addMenuItem(section); ++ } else { ++ const orderIndex = this.sectionOrder.indexOf(sectionName); ++ if (orderIndex !== -1) { ++ // Section is in sectionOrder, add at its position ++ this._menu.addMenuItem(section, orderIndex); ++ } else { ++ // Section not in sectionOrder, add before empty section if it exists ++ const emptySection = this._sections.get(null); ++ if (emptySection) { ++ const emptyIndex = this._menu._getMenuItems().indexOf(emptySection); ++ this._menu.addMenuItem(section, emptyIndex); ++ } else { ++ this._menu.addMenuItem(section); ++ } + } + } + -+ this._button.visible = visibleSections.length > 0; ++ this._sections.set(key, section); ++ ++ return section; ++ } ++ ++ _canBeVisible() { ++ const visibleSections = this._getVisibleSections(); ++ ++ for (const [sectionName, section] of this._sections) ++ section.actor.visible = visibleSections.includes(sectionName); ++ ++ return visibleSections.length > 0; ++ } ++ ++ updateVisibility({visible = true} = {}) { ++ visible &&= this._canBeVisible(); ++ ++ 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() { -+ return Array.from(this._headers.keys()).filter(sectionName => ++ return Array.from(this._sections.keys()).filter(sectionName => + this._getSectionItemCount(sectionName) > 1 + ); + } @@ -3148,24 +3322,28 @@ index 0000000000..b5018d8382 + return JSON.parse(activeKeys[0]); + } + -+ close() { ++ closeMenu() { + this._menu.close(); + } +} + +export class AuthMenuButtonIndicator extends AuthMenuButton { -+ static [GObject.GTypeName] = 'AuthMenuButtonIndicator'; -+ + static { + GObject.registerClass(this); + } + -+ constructor(params) { -+ params.readOnly = true; -+ super(params); ++ constructor(params = {}) { ++ super({ ++ readOnly: true, ++ ...params, ++ }); + -+ this._button.add_style_class_name('login-dialog-auth-menu-button-indicator'); ++ this._createChild(); + ++ this.add_style_class_name('login-dialog-auth-menu-button-indicator'); ++ } ++ ++ _createChild() { + const container = new St.BoxLayout({ + x_align: Clutter.ActorAlign.START, + }); @@ -3175,12 +3353,10 @@ index 0000000000..b5018d8382 + x_expand: true, + x_align: Clutter.ActorAlign.CENTER, + }); -+ this._button.child = this._iconsBox; -+ -+ this.remove_child(this._button); -+ container.add_child(this._button); ++ container.add_child(this._iconsBox); + + this._descriptionLabel = new St.Label({ ++ style_class: 'login-dialog-auth-menu-button-indicator-description', + y_align: Clutter.ActorAlign.CENTER, + }); + this._descriptionLabel.bind_property_full('text', @@ -3191,8 +3367,13 @@ index 0000000000..b5018d8382 + container.add_child(this._descriptionLabel); + + this.child = container; ++ } + -+ this._menu.setSourceActor(this._button); ++ _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) { @@ -3207,10 +3388,6 @@ index 0000000000..b5018d8382 + super.addItem(item); + } + -+ _createMenuItem(item) { -+ return new AuthMenuItemIndicator(item); -+ } -+ + clearItems(searchCriteria = {}) { + this._findItems(searchCriteria).forEach(itemKey => { + const item = JSON.parse(itemKey); @@ -3231,11 +3408,12 @@ index 0000000000..b5018d8382 + } + + // 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 ee6b77d322..254d2af958 100644 +index 041365e5d..d8820876a 100644 --- a/js/js-resources.gresource.xml +++ b/js/js-resources.gresource.xml @@ -2,6 +2,7 @@ @@ -3245,48 +3423,15 @@ index ee6b77d322..254d2af958 100644 + gdm/authMenuButton.js gdm/authPrompt.js gdm/batch.js - gdm/const.js -diff --git a/js/ui/popupMenu.js b/js/ui/popupMenu.js -index 0426ce5452..b719b1bc0c 100644 ---- a/js/ui/popupMenu.js -+++ b/js/ui/popupMenu.js -@@ -1104,6 +1104,28 @@ export class PopupMenu extends PopupMenuBase { - this._boxPointer.setSourceAlignment(alignment); - } - -+ setSourceActor(sourceActor) { -+ if (this.sourceActor === sourceActor) -+ return; -+ -+ this.sourceActor?.disconnectObject(this); -+ -+ this.sourceActor = sourceActor; -+ this.focusActor = sourceActor; -+ -+ if (this.sourceActor) { -+ this.sourceActor.connectObject( -+ 'key-press-event', this._onKeyPress.bind(this), -+ 'notify::mapped', () => { -+ if (!this.sourceActor.mapped) -+ this.close(); -+ }, this); -+ } -+ -+ if (this.isOpen) -+ this._boxPointer.setPosition(this.sourceActor, this._arrowAlignment); -+ } -+ - open(animate) { - if (this.isOpen) - return; + gdm/constants.js -- -2.53.0 +2.54.0 -From a4272f28a818a1cb49b51e62a42198bb49c8fa42 Mon Sep 17 00:00:00 2001 +From 27df463d10c4bfbee415df0bbbb775e77b64e602 Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Tue, 6 Feb 2024 11:11:32 -0500 -Subject: [PATCH 26/42] loginDialog: Port sessions menu over to AuthMenuButton +Subject: [PATCH 35/54] loginDialog: Port sessions menu over to AuthMenuButton Now that AuthMenuButton exists, we should use it. @@ -3294,14 +3439,14 @@ This commit changes the session menu over to use the new control. --- .../gnome-shell-sass/widgets/_login-lock.scss | 1 + - js/gdm/loginDialog.js | 170 ++++++------------ - 2 files changed, 56 insertions(+), 115 deletions(-) + js/gdm/loginDialog.js | 195 ++++++------------ + 2 files changed, 68 insertions(+), 128 deletions(-) diff --git a/data/theme/gnome-shell-sass/widgets/_login-lock.scss b/data/theme/gnome-shell-sass/widgets/_login-lock.scss -index 45d47f0652..c18a6bb8a8 100644 +index 61ec97499..078b5c5c2 100644 --- a/data/theme/gnome-shell-sass/widgets/_login-lock.scss +++ b/data/theme/gnome-shell-sass/widgets/_login-lock.scss -@@ -50,6 +50,7 @@ $_gdm_dialog_width: 25em; +@@ -57,6 +57,7 @@ $_gdm_dialog_width: 25em; &.a11y-button, &.cancel-button, &.switch-user-button, @@ -3310,7 +3455,7 @@ index 45d47f0652..c18a6bb8a8 100644 @extend .icon-button; @extend %system_button; diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js -index 3a22bf161b..46e2fbc126 100644 +index 3a22bf161..9187434a8 100644 --- a/js/gdm/loginDialog.js +++ b/js/gdm/loginDialog.js @@ -27,9 +27,9 @@ import Pango from 'gi://Pango'; @@ -3460,35 +3605,17 @@ index 3a22bf161b..46e2fbc126 100644 this._a11yMenuButton = new A11yMenuButton(); this._bottomButtonGroup.add_child(this._a11yMenuButton); -@@ -688,6 +586,45 @@ export const LoginDialog = GObject.registerClass({ +@@ -688,6 +586,56 @@ export const LoginDialog = GObject.registerClass({ this._updateDisableUserList.bind(this), 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); -+ -+ const ids = Gdm.get_session_ids(); -+ ids.sort(); -+ -+ if (ids.length <= 1) { -+ this._button.hide(); -+ return; -+ } -+ -+ for (const id of ids) { -+ const [sessionName, _] = Gdm.get_session_name_and_description(id); -+ -+ this._authMenuButton.addItem({ -+ sectionName: _SESSION_TYPE_SECTION_NAME, -+ name: sessionName, -+ id, -+ }); -+ } + + this._authMenuButton.connect('active-item-changed', (_button, sectionName) => { + const item = this._authMenuButton.getActiveItem({sectionName}); @@ -3498,27 +3625,56 @@ index 3a22bf161b..46e2fbc126 100644 + if (sectionName === _SESSION_TYPE_SECTION_NAME) + this._greeter.call_select_session_sync(item.id, null); + -+ this._authMenuButton.close(); ++ this._authMenuButton.closeMenu(); + }); + this._bottomButtonGroup.add_child(this._authMenuButton); + } ++ ++ _updateSessions() { ++ this._authMenuButton.clearItems({ ++ 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 === 0) ++ return; ++ ++ const sessions = ids.map(id => { ++ const [sessionName] = Gdm.get_session_name_and_description(id); ++ return {id, sessionName}; ++ }); ++ ++ sessions.sort((a, b) => a.sessionName.localeCompare(b.sessionName)); ++ ++ for (const {id, sessionName} of sessions) { ++ this._authMenuButton.addItem({ ++ sectionName: _SESSION_TYPE_SECTION_NAME, ++ name: sessionName, ++ id, ++ }); ++ } ++ } + _getBannerAllocation(dialogBox) { let actorBox = new Clutter.ActorBox(); -@@ -1061,10 +998,8 @@ export const LoginDialog = GObject.registerClass({ +@@ -1061,10 +1009,8 @@ export const 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(); } -@@ -1091,7 +1026,6 @@ export const LoginDialog = GObject.registerClass({ +@@ -1091,7 +1037,6 @@ export const LoginDialog = GObject.registerClass({ _onReset(authPrompt, resetType) { this._ensureGreeterProxy(); @@ -3526,54 +3682,115 @@ index 3a22bf161b..46e2fbc126 100644 const previousUser = this._user; this._user = null; -@@ -1125,11 +1059,18 @@ export const LoginDialog = GObject.registerClass({ +@@ -1125,23 +1070,15 @@ export const 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, -@@ -1191,7 +1132,7 @@ export const LoginDialog = GObject.registerClass({ + _showPrompt() { +@@ -1185,13 +1122,14 @@ export const 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._authMenuButton.updateVisibility({visible: false}); this._authPrompt.updateSensitivity({sensitive: true}); this._showPrompt(); } -@@ -1505,8 +1446,7 @@ export const LoginDialog = GObject.registerClass({ +@@ -1505,8 +1443,7 @@ export const 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(); +@@ -1525,6 +1462,8 @@ export const LoginDialog = GObject.registerClass({ + _onUserListActivated(activatedItem) { + this._user = activatedItem.user; + ++ this._updateSessions(); ++ + this._updateCancelButton(); + + if (this._conflictingSessionNotification) -- -2.53.0 +2.54.0 -From c4d9263b2270b43fb6f538243277f90559fb8f57 Mon Sep 17 00:00:00 2001 +From 4246ce5f377c097ef55f3ec7c5016da0363dea9b Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Wed, 27 May 2026 18:27:35 +0200 +Subject: [PATCH 36/54] loginDialog: Use promisify with + this._greeter.call_select_session() + +--- + js/gdm/loginDialog.js | 3 ++- + 1 file changed, 2 insertions(+), 1 deletion(-) + +diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js +index 9187434a8..5441e2dda 100644 +--- a/js/gdm/loginDialog.js ++++ b/js/gdm/loginDialog.js +@@ -53,6 +53,7 @@ const _SESSION_TYPE_SECTION_NAME = _('Session Type'); + const N_A11Y_MENU_COLUMNS = 2; + + Gio._promisify(Gio.File.prototype, 'load_contents_async'); ++Gio._promisify(Gdm.Greeter.prototype, 'call_select_session'); + + export const UserListItem = GObject.registerClass({ + Signals: {'activate': {}}, +@@ -600,7 +601,7 @@ export const LoginDialog = GObject.registerClass({ + return; + + if (sectionName === _SESSION_TYPE_SECTION_NAME) +- this._greeter.call_select_session_sync(item.id, null); ++ this._greeter.call_select_session(item.id, null).catch(logError); + + this._authMenuButton.closeMenu(); + }); +-- +2.54.0 + + +From 04c458da6c4139e40cf2b45c3c04fd62312d4306 Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Tue, 6 Feb 2024 13:40:26 -0500 -Subject: [PATCH 27/42] loginDialog: Add login options menu to AuthMenuButton +Subject: [PATCH 37/54] loginDialog: Add login options menu to AuthMenuButton MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @@ -3586,24 +3803,25 @@ authPrompt is in charge of: signal, it also provides one between them that is selected by default. * Getting the selected login mechanism with "selectMechanism" method. -In future commits it will be implemented how UserVerifier emits -"mechanisms-changed". +In future commits the "mechanisms-selected" signal and the "selectMechanism" +method of UserVerifier will be implemented. There are some mechanisms that are not selectable but run in the background, e.g. fingerprint. These ones won't be at _authMenuButton but -at the new _authIndicatorButton to inform when they are enabled. +at the future _authIndicatorButton to inform when they are enabled. Co-authored-by: Marco Trevisan (Treviño) --- - js/gdm/authPrompt.js | 13 ++++++++++++ - js/gdm/loginDialog.js | 48 ++++++++++++++++++++++++++++++++++++------- - 2 files changed, 54 insertions(+), 7 deletions(-) + js/gdm/authPrompt.js | 20 ++++++++++++++++++++ + js/gdm/loginDialog.js | 43 +++++++++++++++++++++++++++++++++++++++++-- + js/gdm/util.js | 5 +++++ + 3 files changed, 66 insertions(+), 2 deletions(-) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index 7b3aa8558f..11d90a4de3 100644 +index a5b7132a3..c8c9e55ec 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -51,6 +51,7 @@ export const AuthPrompt = GObject.registerClass({ +@@ -50,6 +50,7 @@ export const AuthPrompt = GObject.registerClass({ 'failed': {}, 'next': {}, 'prompted': {}, @@ -3611,7 +3829,7 @@ index 7b3aa8558f..11d90a4de3 100644 'reset': {param_types: [GObject.TYPE_UINT]}, 'verification-complete': {}, 'loading': {param_types: [GObject.TYPE_BOOLEAN]}, -@@ -87,6 +88,7 @@ export const AuthPrompt = GObject.registerClass({ +@@ -86,6 +87,7 @@ export const AuthPrompt = GObject.registerClass({ 'ask-question', this._onAskQuestion.bind(this), 'show-message', this._onShowMessage.bind(this), 'show-choice-list', this._onShowChoiceList.bind(this), @@ -3619,7 +3837,7 @@ index 7b3aa8558f..11d90a4de3 100644 'verification-failed', this._onVerificationFailed.bind(this), 'verification-complete', this._onVerificationComplete.bind(this), 'reset', this._onReset.bind(this), -@@ -772,6 +774,17 @@ export const AuthPrompt = GObject.registerClass({ +@@ -771,6 +773,24 @@ export const AuthPrompt = GObject.registerClass({ this.updateSensitivity(false); } @@ -3631,6 +3849,13 @@ index 7b3aa8558f..11d90a4de3 100644 + if (invalidStatus.includes(this.verificationStatus)) + return false; + ++ const oldPromptStep = this._promptStep; ++ this._promptStep = 0; ++ if (!this._userVerifier.selectMechanism(mechanism)) ++ this._promptStep = oldPromptStep; ++ ++ this._updateCancelButton(); ++ + return true; + } + @@ -3638,7 +3863,7 @@ index 7b3aa8558f..11d90a4de3 100644 let {reuseEntryText, softReset} = Params.parse(params, { reuseEntryText: false, diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js -index 46e2fbc126..ddb7e603b2 100644 +index 5441e2dda..8656bdf29 100644 --- a/js/gdm/loginDialog.js +++ b/js/gdm/loginDialog.js @@ -48,6 +48,7 @@ const _SCROLL_ANIMATION_TIME = 500; @@ -3649,7 +3874,7 @@ index 46e2fbc126..ddb7e603b2 100644 const _SESSION_TYPE_SECTION_NAME = _('Session Type'); const N_A11Y_MENU_COLUMNS = 2; -@@ -497,6 +498,7 @@ export const LoginDialog = GObject.registerClass({ +@@ -498,6 +499,7 @@ export const 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)); @@ -3657,27 +3882,16 @@ index 46e2fbc126..ddb7e603b2 100644 this._authPrompt.hide(); this.add_child(this._authPrompt); -@@ -590,18 +592,13 @@ export const LoginDialog = GObject.registerClass({ +@@ -590,7 +592,7 @@ export const 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); - - const ids = Gdm.get_session_ids(); - ids.sort(); - -- if (ids.length <= 1) { -- this._button.hide(); -- return; -- } -- - for (const id of ids) { - const [sessionName, _] = Gdm.get_session_name_and_description(id); - -@@ -617,7 +614,9 @@ export const LoginDialog = GObject.registerClass({ +@@ -600,7 +602,9 @@ export const LoginDialog = GObject.registerClass({ if (!item) return; @@ -3685,11 +3899,11 @@ index 46e2fbc126..ddb7e603b2 100644 + if (sectionName === _PRIMARY_LOGIN_METHOD_SECTION_NAME) + this._selectAuthMechanism(item); + else if (sectionName === _SESSION_TYPE_SECTION_NAME) - this._greeter.call_select_session_sync(item.id, null); + this._greeter.call_select_session(item.id, null).catch(logError); - this._authMenuButton.close(); -@@ -625,6 +624,20 @@ export const LoginDialog = GObject.registerClass({ - this._bottomButtonGroup.add_child(this._authMenuButton); + this._authMenuButton.closeMenu(); +@@ -637,6 +641,20 @@ export const LoginDialog = GObject.registerClass({ + } } + _selectAuthMechanism(authMechanism) { @@ -3709,8 +3923,8 @@ index 46e2fbc126..ddb7e603b2 100644 _getBannerAllocation(dialogBox) { let actorBox = new Clutter.ActorBox(); -@@ -1063,6 +1076,27 @@ export const LoginDialog = GObject.registerClass({ - this._authMenuButton.updateReactive(!isLoading); +@@ -1075,6 +1093,27 @@ export const LoginDialog = GObject.registerClass({ + this._authMenuButton.reactive = !isLoading; } + _onMechanismsChanged(_authPrompt, mechanisms, selectedMechanism) { @@ -3737,14 +3951,30 @@ index 46e2fbc126..ddb7e603b2 100644 _onDefaultSessionChanged(client, sessionId) { this._authMenuButton.setActiveItem({ sectionName: _SESSION_TYPE_SECTION_NAME, +diff --git a/js/gdm/util.js b/js/gdm/util.js +index f0b99a0ff..f6ff37603 100644 +--- a/js/gdm/util.js ++++ b/js/gdm/util.js +@@ -219,6 +219,11 @@ export class ShellUserVerifier extends Signals.EventEmitter { + this._getUserVerifier(); + } + ++ selectMechanism(mechanism) { ++ // TODO: Implement mechanism selection ++ return false; ++ } ++ + cancel() { + if (this._cancellable) + this._cancellable.cancel(); -- -2.53.0 +2.54.0 -From 25db638e4f11984258ed9efebb5ead49f01045cb Mon Sep 17 00:00:00 2001 +From 217f5b771d5f6646f98413282110f6bb304e18c7 Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Tue, 6 Feb 2024 13:41:39 -0500 -Subject: [PATCH 28/42] unlockDialog: Add _authMenuButton and +Subject: [PATCH 38/54] unlockDialog: Add _authMenuButton and _authIndicatorButton _authMenuButton is used to select an available auth mechanism from the @@ -3755,14 +3985,12 @@ 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. --- js/ui/unlockDialog.js | 158 +++++++++++++++++++++++++++++++++++++----- - 1 file changed, 141 insertions(+), 17 deletions(-) + 1 file changed, 140 insertions(+), 18 deletions(-) diff --git a/js/ui/unlockDialog.js b/js/ui/unlockDialog.js -index fffc58e07c..af5a238d87 100644 +index 100990acc..2c2fb1c54 100644 --- a/js/ui/unlockDialog.js +++ b/js/ui/unlockDialog.js @@ -15,11 +15,16 @@ import * as Main from './main.js'; @@ -3771,7 +3999,7 @@ index fffc58e07c..af5a238d87 100644 import {formatDateWithCFormatString} from '../misc/dateUtils.js'; +import * as AuthMenuButton from '../gdm/authMenuButton.js'; import * as AuthPrompt from '../gdm/authPrompt.js'; -+import * as GdmConst from '../gdm/const.js'; ++import * as GdmConstants from '../gdm/constants.js'; +import * as GdmUtil from '../gdm/util.js'; import {AuthPromptStatus} from '../gdm/authPrompt.js'; import {MprisSource} from './mpris.js'; @@ -3798,7 +4026,7 @@ index fffc58e07c..af5a238d87 100644 } vfunc_get_preferred_width(container, forHeight) { -@@ -497,22 +503,40 @@ class UnlockDialogLayout extends Clutter.LayoutManager { +@@ -489,22 +495,40 @@ class UnlockDialogLayout extends Clutter.LayoutManager { this._stack.allocate(actorBox); @@ -3806,19 +4034,37 @@ index fffc58e07c..af5a238d87 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; @@ -3826,35 +4072,18 @@ index fffc58e07c..af5a238d87 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); } } }); -@@ -622,19 +646,48 @@ export const UnlockDialog = GObject.registerClass({ +@@ -614,19 +638,46 @@ export const 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({ @@ -3868,14 +4097,14 @@ index fffc58e07c..af5a238d87 100644 - 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, + }); + this._authMenuButton.connect('active-item-changed', () => { + const authMechanism = this._authMenuButton.getActiveItem(); @@ -3883,23 +4112,21 @@ index fffc58e07c..af5a238d87 100644 + return; + + this._selectAuthMechanism(authMechanism); -+ this._authMenuButton.close(); ++ 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.add_style_class_name('login-dialog-bottom-button-group'); + this._authIndicatorButton.set_pivot_point(0.5, 0.5); -+ this._authIndicatorButton.updateSensitivity(true); this._screenSaverSettings = new Gio.Settings({schema_id: 'org.gnome.desktop.screensaver'}); -@@ -657,11 +710,13 @@ export const UnlockDialog = GObject.registerClass({ +@@ -649,11 +700,13 @@ export const UnlockDialog = GObject.registerClass({ mainBox.add_constraint(new Layout.MonitorConstraint({primary: true})); mainBox.add_child(this._stack); mainBox.add_child(this._notificationsBox); @@ -3915,7 +4142,7 @@ index fffc58e07c..af5a238d87 100644 this.add_child(mainBox); this._idleMonitor = global.backend.get_core_idle_monitor(); -@@ -699,6 +754,20 @@ export const UnlockDialog = GObject.registerClass({ +@@ -691,6 +744,20 @@ export const UnlockDialog = GObject.registerClass({ return Clutter.EVENT_PROPAGATE; } @@ -3936,7 +4163,7 @@ index fffc58e07c..af5a238d87 100644 _createBackground(monitorIndex) { let monitor = Main.layoutManager.monitors[monitorIndex]; let widget = new St.Widget({ -@@ -755,6 +824,8 @@ export const UnlockDialog = GObject.registerClass({ +@@ -747,6 +814,8 @@ export const UnlockDialog = GObject.registerClass({ this._authPrompt.connect('failed', this._fail.bind(this)); this._authPrompt.connect('cancelled', this._fail.bind(this)); this._authPrompt.connect('reset', this._onReset.bind(this)); @@ -3945,7 +4172,7 @@ index fffc58e07c..af5a238d87 100644 this._promptBox.add_child(this._authPrompt); } -@@ -817,6 +888,12 @@ export const UnlockDialog = GObject.registerClass({ +@@ -809,6 +878,12 @@ export const UnlockDialog = GObject.registerClass({ reactive: progress > 0, can_focus: progress > 0, }); @@ -3958,7 +4185,7 @@ index fffc58e07c..af5a238d87 100644 const {scaleFactor} = St.ThemeContext.get_for_stage(global.stage); -@@ -834,7 +911,7 @@ export const UnlockDialog = GObject.registerClass({ +@@ -826,7 +901,7 @@ export const UnlockDialog = GObject.registerClass({ translation_y: -FADE_OUT_TRANSLATION * progress * scaleFactor, }); @@ -3967,12 +4194,12 @@ index fffc58e07c..af5a238d87 100644 opacity: 255 * progress, scale_x: FADE_OUT_SCALE + (1 - FADE_OUT_SCALE) * progress, scale_y: FADE_OUT_SCALE + (1 - FADE_OUT_SCALE) * progress, -@@ -858,6 +935,52 @@ export const UnlockDialog = GObject.registerClass({ +@@ -850,6 +925,52 @@ export const UnlockDialog = GObject.registerClass({ this._authPrompt.begin({userName}); } + _onLoading(_authPrompt, isLoading) { -+ this._authMenuButton.updateReactive(!isLoading); ++ this._authMenuButton.reactive = !isLoading; + } + + _onMechanismsChanged(_authPrompt, mechanisms, selectedMechanism) { @@ -3993,7 +4220,7 @@ index fffc58e07c..af5a238d87 100644 + }); + } else { + this._authIndicatorButton.addItem({ -+ iconName: GdmUtil.getIconName(m), ++ iconName: GdmUtil.getNonSelectableIconName(m), + description: this._getUnlockDescription(m), + ...m, + }); @@ -4010,7 +4237,7 @@ index fffc58e07c..af5a238d87 100644 + // This is only used for non selectable mechanisms. + // Currently only fingerprint is non selectable + switch (mechanism.role) { -+ case GdmConst.FINGERPRINT_ROLE_NAME: ++ case GdmConstants.FINGERPRINT_ROLE_NAME: + return _('Unlock with fingerprint'); + default: + throw new Error(`Failed getting unlock description: ${mechanism.role}`); @@ -4020,7 +4247,7 @@ index fffc58e07c..af5a238d87 100644 _escape() { if (this._authPrompt && this.allowCancel) this._authPrompt.cancel(); -@@ -921,7 +1044,8 @@ export const UnlockDialog = GObject.registerClass({ +@@ -912,7 +1033,8 @@ export const UnlockDialog = GObject.registerClass({ this._otherUserButton.visible = this._userManager.can_switch() && this._userManager.has_multiple_users && this._screenSaverSettings.get_boolean('user-switch-enabled') && @@ -4031,35 +4258,41 @@ index fffc58e07c..af5a238d87 100644 cancel() { -- -2.53.0 +2.54.0 -From 8256dce648e05285a4bf40b98d547e585f6e3d06 Mon Sep 17 00:00:00 2001 +From e240976e3c207b232e968a5323f36535a67c55ab Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Tue, 3 Dec 2024 07:39:32 -0500 -Subject: [PATCH 29/42] unlockDialog: Update hint text based on mockup +Subject: [PATCH 39/54] 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. --- - 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 af5a238d87..80c3dd813a 100644 +index 2c2fb1c54..af584470a 100644 --- a/js/ui/unlockDialog.js +++ b/js/ui/unlockDialog.js -@@ -423,9 +423,17 @@ class UnlockDialogClock extends St.BoxLayout { +@@ -422,10 +422,23 @@ class UnlockDialogClock extends St.BoxLayout { + this._date.text = formatDateWithCFormatString(date, 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 === GdmConst.SMARTCARD_ROLE_NAME) ++ if (selectedAuthRole === GdmConstants.SMARTCARD_ROLE_NAME) + text = _('Insert smartcard'); + else if (this._seat.touch_mode) + text = _('Swipe up'); @@ -4070,52 +4303,64 @@ index af5a238d87..80c3dd813a 100644 } _onDestroy() { +@@ -756,6 +769,8 @@ export const UnlockDialog = GObject.registerClass({ + } + + this._selectedAuthMechanism = authMechanism; ++ ++ this._clock.selectAuthRole(authMechanism?.role); + } + + _createBackground(monitorIndex) { -- -2.53.0 +2.54.0 -From 52f38a7b2d63a96f3af2e1ec87f3832821094fdd Mon Sep 17 00:00:00 2001 +From 5ff80788f94268039f982f757d2c7b80dd88e152 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Tue, 14 Oct 2025 19:19:15 +0200 -Subject: [PATCH 30/42] gdm/util: Increase time of messages based on new +Subject: [PATCH 40/54] gdm/util: Increase time of messages based on new environment variable -called 'GDM_MESSAGE_TIME_MULTIPLIER'. This is can used for testing +called 'GDM_MESSAGE_TIME_MULTIPLIER'. This is used for testing purposes. When no set, the multiplier is 1 which does nothing. --- - js/gdm/util.js | 4 +++- - 1 file changed, 3 insertions(+), 1 deletion(-) + 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 660c6e4ddc..12dec5dd46 100644 +index f6ff37603..5eadcd011 100644 --- a/js/gdm/util.js +++ b/js/gdm/util.js -@@ -43,6 +43,7 @@ export const DISABLE_USER_LIST_KEY = 'disable-user-list'; +@@ -44,6 +44,10 @@ export const DISABLE_USER_LIST_KEY = 'disable-user-list'; // or 2 seconds, whichever is longer const USER_READ_TIME = 48; const USER_READ_TIME_MIN = 2000; -+const MESSAGE_TIME_MULTIPLIER = GLib.getenv('GDM_MESSAGE_TIME_MULTIPLIER') ?? 1; ++const MESSAGE_TIME_MULTIPLIER = (() => { ++ const value = Number.parseFloat(GLib.getenv('GDM_MESSAGE_TIME_MULTIPLIER')); ++ return Number.isFinite(value) && value > 0 ? value : 1; ++})(); const FINGERPRINT_SERVICE_PROXY_TIMEOUT = 5000; const FINGERPRINT_ERROR_TIMEOUT_WAIT = 15; -@@ -278,7 +279,8 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -287,7 +291,8 @@ export class ShellUserVerifier extends Signals.EventEmitter { return 0; // We probably could be smarter here - return Math.max(message.length * USER_READ_TIME, USER_READ_TIME_MIN); -+ return Math.max(message.length * USER_READ_TIME * MESSAGE_TIME_MULTIPLIER, -+ USER_READ_TIME_MIN); ++ return Math.max(message.length * USER_READ_TIME, USER_READ_TIME_MIN) * ++ MESSAGE_TIME_MULTIPLIER; } finishMessageQueue() { -- -2.53.0 +2.54.0 -From b49076f274bc33e32e52747e6858d28fb029a1fc Mon Sep 17 00:00:00 2001 +From b47fa82b3b6ce5d9dbe23846e95713d26a57b60f Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Mon, 15 Sep 2025 17:13:17 +0200 -Subject: [PATCH 31/42] gdm/util: Allow null _hold and don't recreate dummy +Subject: [PATCH 41/54] gdm/util: Allow null _hold and don't recreate dummy holds _hold property is used to inform the caller of begin method (authPrompt) @@ -4134,10 +4379,10 @@ but for now, just accept getting null and dont create dummy ones. 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index 11d90a4de3..9a9c0c3802 100644 +index c8c9e55ec..ff51c05f3 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -858,11 +858,7 @@ export const AuthPrompt = GObject.registerClass({ +@@ -863,11 +863,7 @@ export const AuthPrompt = GObject.registerClass({ this.updateSensitivity({sensitive: false}); @@ -4151,19 +4396,19 @@ index 11d90a4de3..9a9c0c3802 100644 } diff --git a/js/gdm/util.js b/js/gdm/util.js -index 12dec5dd46..247048ddab 100644 +index 5eadcd011..45ee4efda 100644 --- a/js/gdm/util.js +++ b/js/gdm/util.js -@@ -462,8 +462,6 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -474,8 +474,6 @@ export class ShellUserVerifier extends Signals.EventEmitter { if (this._userVerifier && - !this._activeServices.has(Const.FINGERPRINT_SERVICE_NAME)) { + !this._activeServices.has(Constants.FINGERPRINT_SERVICE_NAME)) { - if (!this._hold?.isAcquired()) - this._hold = new Batch.Hold(); await this._maybeStartFingerprintVerification(); } } -@@ -527,7 +525,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -539,7 +537,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { _reportInitError(where, error, serviceName) { logError(error, where); @@ -4172,7 +4417,7 @@ index 12dec5dd46..247048ddab 100644 this._queueMessage(serviceName, _('Authentication error'), MessageType.ERROR); this._failCounter++; -@@ -565,7 +563,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -577,7 +575,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { this.reauthenticating = true; this._connectSignals(); this._beginVerification(); @@ -4181,7 +4426,7 @@ index 12dec5dd46..247048ddab 100644 } async _getUserVerifier() { -@@ -589,7 +587,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -601,7 +599,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { this._connectSignals(); this._beginVerification(); @@ -4190,7 +4435,7 @@ index 12dec5dd46..247048ddab 100644 } _connectSignals() { -@@ -707,7 +705,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -719,7 +717,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { } async _startService(serviceName) { @@ -4199,7 +4444,7 @@ index 12dec5dd46..247048ddab 100644 try { this._activeServices.add(serviceName); if (this._userName) { -@@ -724,7 +722,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -736,7 +734,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { if (!this.serviceIsForeground(serviceName)) { logError(e, `Failed to start ${serviceName} for ${this._userName}`); @@ -4208,7 +4453,7 @@ index 12dec5dd46..247048ddab 100644 return; } this._reportInitError( -@@ -734,7 +732,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -746,7 +744,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { e, serviceName); return; } @@ -4217,7 +4462,7 @@ index 12dec5dd46..247048ddab 100644 } _beginVerification() { -@@ -867,7 +865,6 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -879,7 +877,6 @@ export class ShellUserVerifier extends Signals.EventEmitter { } _retry(serviceName) { @@ -4226,13 +4471,13 @@ index 12dec5dd46..247048ddab 100644 this._startService(serviceName); } -- -2.53.0 +2.54.0 -From 50aef73235bd66243f1472a24711840beea5eb27 Mon Sep 17 00:00:00 2001 +From 6798ef06bac6d0d307f6e2c766ca7e0158d8c83d Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Wed, 20 Aug 2025 10:54:33 +0200 -Subject: [PATCH 32/42] gdm/util: Add fingerprintManager +Subject: [PATCH 42/54] gdm: Add FingerprintManager Move fingerprint bits to new fingerprintManager class. @@ -4242,26 +4487,170 @@ 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 | 154 +++++++++------------------------- + js/gdm/fingerprintManager.js | 138 +++++++++++++++++++++++++++++++ + js/gdm/util.js | 149 ++++++++-------------------------- js/js-resources.gresource.xml | 1 + - js/misc/fingerprintManager.js | 140 +++++++++++++++++++++++++++++++ - 3 files changed, 179 insertions(+), 116 deletions(-) - create mode 100644 js/misc/fingerprintManager.js + 3 files changed, 173 insertions(+), 115 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 000000000..2577facbb +--- /dev/null ++++ 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 '../misc/fileUtils.js'; ++ ++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 = { ++ NONE: 0, ++ PRESS: 1, ++ SWIPE: 2, ++}; ++ ++let _fingerprintManager = null; ++ ++/** ++ * @returns {FingerprintManager} ++ */ ++export function getFingerprintManager() { ++ if (_fingerprintManager == null) ++ _fingerprintManager = new FingerprintManager(); ++ ++ return _fingerprintManager; ++} ++ ++class FingerprintManager extends GObject.Object { ++ static [GObject.properties] = { ++ 'reader-type': GObject.ParamSpec.uint( ++ 'reader-type', null, null, ++ GObject.ParamFlags.READWRITE, ++ FingerprintReaderType.NONE, FingerprintReaderType.SWIPE, ++ FingerprintReaderType.NONE), ++ }; ++ ++ static [GObject.signals] = { ++ 'reader-type-changed': {}, ++ }; ++ ++ static { ++ GObject.registerClass(this); ++ } ++ ++ constructor() { ++ super(); ++ ++ this._fingerprintManagerProxy = 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, ++ }); ++ ++ this._initFingerprintManagerProxy(); ++ } ++ ++ get readerFound() { ++ return this.readerType !== FingerprintReaderType.NONE; ++ } ++ ++ setDefaultTimeout(timeout) { ++ this._fingerprintManagerProxy.set_default_timeout(timeout); ++ } ++ ++ async checkReaderType(cancellable = null) { ++ try { ++ // Wrappers don't support null cancellable, so let's ignore it in case ++ const args = cancellable ? [cancellable] : []; ++ const [devicePath] = ++ await this._fingerprintManagerProxy.GetDefaultDeviceAsync(...args); ++ ++ const fprintDeviceProxy = this._getFprintDeviceProxy(devicePath); ++ await fprintDeviceProxy.init_async(GLib.PRIORITY_DEFAULT, cancellable ?? null); ++ ++ const fingerprintReaderKey = fprintDeviceProxy['scan-type'].toUpperCase(); ++ const fingerprintReaderType = FingerprintReaderType[fingerprintReaderKey]; ++ this._setFingerprintReaderType(fingerprintReaderType); ++ } catch (e) { ++ if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) ++ return; ++ this._handleFingerprintError(e); ++ } ++ } ++ ++ async _initFingerprintManagerProxy() { ++ try { ++ await this._fingerprintManagerProxy.init_async( ++ GLib.PRIORITY_DEFAULT, null); ++ await this.checkReaderType(); ++ } catch (e) { ++ this._handleFingerprintError(e); ++ } ++ } ++ ++ _getFprintDeviceProxy(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, ++ }); ++ } ++ ++ _setFingerprintReaderType(fingerprintReaderType) { ++ if (fingerprintReaderType === undefined) ++ throw new Error(`Unexpected fingerprint device type '${fingerprintReaderType}'`); ++ ++ if (this.readerType === fingerprintReaderType) ++ return; ++ ++ this.readerType = fingerprintReaderType; ++ this.emit('reader-type-changed'); ++ } ++ ++ _handleFingerprintError(e) { ++ this._setFingerprintReaderType(FingerprintReaderType.NONE); ++ ++ if (e instanceof GLib.Error) { ++ if (e.matches(Gio.DBusError, Gio.DBusError.SERVICE_UNKNOWN)) ++ 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'); ++ } ++} diff --git a/js/gdm/util.js b/js/gdm/util.js -index 247048ddab..ae464665e2 100644 +index 45ee4efda..a1c4ff939 100644 --- a/js/gdm/util.js +++ b/js/gdm/util.js -@@ -5,6 +5,8 @@ import GLib from 'gi://GLib'; - import * as Signals from '../misc/signals.js'; +@@ -6,19 +6,14 @@ import * as Signals from '../misc/signals.js'; import * as Batch from './batch.js'; -+import * as Const from './const.js'; -+import * as FingerprintManager from '../misc/fingerprintManager.js'; + import * as Constants from './constants.js'; ++import {FingerprintManager, FingerprintReaderType} from './fingerprintManager.js'; import * as OVirt from './oVirt.js'; import * as Vmware from './vmware.js'; import * as Main from '../ui/main.js'; -@@ -13,11 +15,6 @@ import {loadInterfaceXML} from '../misc/fileUtils.js'; + import {logErrorUnlessCancelled} from '../misc/errorUtils.js'; +-import {loadInterfaceXML} from '../misc/fileUtils.js'; import * as Params from '../misc/params.js'; import * as SmartcardManager from '../misc/smartcardManager.js'; @@ -4273,7 +4662,7 @@ index 247048ddab..ae464665e2 100644 Gio._promisify(Gdm.Client.prototype, 'open_reauthentication_channel'); Gio._promisify(Gdm.Client.prototype, 'get_user_verifier'); Gio._promisify(Gdm.UserVerifierProxy.prototype, -@@ -60,12 +57,6 @@ export const MessageType = { +@@ -64,12 +59,6 @@ export const MessageType = { ERROR: 3, }; @@ -4286,17 +4675,15 @@ index 247048ddab..ae464665e2 100644 /** * @param {Clutter.Actor} actor */ -@@ -141,7 +132,8 @@ export class ShellUserVerifier extends Signals.EventEmitter { - +@@ -149,6 +138,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { this._defaultService = null; this._preemptingService = null; -- this._fingerprintReaderType = FingerprintReaderType.NONE; -+ this._fingerprintReaderType = FingerprintManager.FingerprintReaderType.NONE; + this._fingerprintReaderType = FingerprintReaderType.NONE; + this._fingerprintReaderFound = false; this._messageQueue = []; this._messageQueueTimeoutId = 0; -@@ -205,8 +197,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -212,8 +202,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { this._userName = userName; this.reauthenticating = false; @@ -4306,7 +4693,7 @@ index 247048ddab..ae464665e2 100644 // If possible, reauthenticate an already running session, // so any session specific credentials get updated appropriately -@@ -255,6 +246,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -267,6 +256,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { this._smartcardManager?.disconnectObject(this); this._smartcardManager = null; @@ -4314,7 +4701,7 @@ index 247048ddab..ae464665e2 100644 this._fingerprintManager = null; for (let service in this._credentialManagers) -@@ -366,112 +358,40 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -378,112 +368,39 @@ export class ShellUserVerifier extends Signals.EventEmitter { } async _initFingerprintManager() { @@ -4391,8 +4778,7 @@ index 247048ddab..ae464665e2 100644 - - logError(e, 'Failed to interact with fprintd service'); - } -+ this._fingerprintManager = -+ new FingerprintManager.FingerprintManager(this._cancellable); ++ this._fingerprintManager = new FingerprintManager(this._cancellable); + this._fingerprintManager.connectObject( + 'reader-type-changed', () => this._onFingerprintReaderTypeChanged(), + this); @@ -4436,7 +4822,7 @@ index 247048ddab..ae464665e2 100644 this._updateDefaultService(); if (this._userVerifier && -- !this._activeServices.has(Const.FINGERPRINT_SERVICE_NAME)) { +- !this._activeServices.has(Constants.FINGERPRINT_SERVICE_NAME)) { - await this._maybeStartFingerprintVerification(); - } - } @@ -4447,21 +4833,21 @@ index 247048ddab..ae464665e2 100644 - - if (this._fingerprintReaderType === undefined) - throw new Error(`Unexpected fingerprint device type '${fprintDeviceType}'`); -+ !this._activeServices.has(Const.FINGERPRINT_SERVICE_NAME)) ++ !this._activeServices.has(Constants.FINGERPRINT_SERVICE_NAME)) + this._maybeStartFingerprintVerification(); } _onCredentialManagerAuthenticated(credentialManager, _token) { -@@ -641,7 +561,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -653,7 +570,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { } serviceIsFingerprint(serviceName) { - return this._fingerprintReaderType !== FingerprintReaderType.NONE && + return this._fingerprintReaderFound && - serviceName === Const.FINGERPRINT_SERVICE_NAME; + serviceName === Constants.FINGERPRINT_SERVICE_NAME; } -@@ -654,9 +574,11 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -666,9 +583,11 @@ export class ShellUserVerifier extends Signals.EventEmitter { let needsReset = false; if (this._settings.get_boolean(FINGERPRINT_AUTHENTICATION_KEY)) { @@ -4473,200 +4859,45 @@ index 247048ddab..ae464665e2 100644 + this._fingerprintReaderFound = false; this._fingerprintReaderType = FingerprintReaderType.NONE; - if (this._activeServices.has(Const.FINGERPRINT_SERVICE_NAME)) -@@ -684,7 +606,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { - return Const.PASSWORD_SERVICE_NAME; + if (this._activeServices.has(Constants.FINGERPRINT_SERVICE_NAME)) +@@ -696,7 +615,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { + return Constants.PASSWORD_SERVICE_NAME; else if (this._smartcardManager) - return Const.SMARTCARD_SERVICE_NAME; + return Constants.SMARTCARD_SERVICE_NAME; - else if (this._fingerprintReaderType !== FingerprintReaderType.NONE) + else if (this._fingerprintReaderFound) - return Const.FINGERPRINT_SERVICE_NAME; + return Constants.FINGERPRINT_SERVICE_NAME; return null; } -@@ -742,7 +664,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -754,7 +673,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { async _maybeStartFingerprintVerification() { if (this._userName && - this._fingerprintReaderType !== FingerprintReaderType.NONE && + this._fingerprintReaderFound && - !this.serviceIsForeground(Const.FINGERPRINT_SERVICE_NAME)) - await this._startService(Const.FINGERPRINT_SERVICE_NAME); + !this.serviceIsForeground(Constants.FINGERPRINT_SERVICE_NAME)) + await this._startService(Constants.FINGERPRINT_SERVICE_NAME); } -@@ -765,7 +687,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { - // We don't show fingerprint messages directly since it's - // not the main auth service. Instead we use the messages - // as a cue to display our own message. -- if (this._fingerprintReaderType === FingerprintReaderType.SWIPE) { -+ if (this._fingerprintReaderType === FingerprintManager.FingerprintReaderType.SWIPE) { - // Translators: this message is shown below the password entry field - // to indicate the user can swipe their finger on the fingerprint reader - this._queueMessage(serviceName, _('(or swipe finger across reader)'), diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml -index 254d2af958..2712bfcc84 100644 +index d8820876a..50e0b179a 100644 --- a/js/js-resources.gresource.xml +++ b/js/js-resources.gresource.xml -@@ -26,6 +26,7 @@ - misc/dependencies.js - misc/errorUtils.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 -new file mode 100644 -index 0000000000..5bcb7bf6f0 ---- /dev/null -+++ b/js/misc/fingerprintManager.js -@@ -0,0 +1,140 @@ -+import Gio from 'gi://Gio'; -+import GLib from 'gi://GLib'; -+import GObject from 'gi://GObject'; -+ -+import {loadInterfaceXML} from './fileUtils.js'; -+ -+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 = { -+ NONE: 0, -+ PRESS: 1, -+ SWIPE: 2, -+}; -+ -+let _fingerprintManager = null; -+ -+/** -+ * @returns {FingerprintManager} -+ */ -+export function getFingerprintManager() { -+ if (_fingerprintManager == null) -+ _fingerprintManager = new FingerprintManager(); -+ -+ return _fingerprintManager; -+} -+ -+class FingerprintManager extends GObject.Object { -+ static [GObject.GTypeName] = 'FingerprintManager'; -+ -+ static [GObject.properties] = { -+ 'reader-type': GObject.ParamSpec.uint( -+ 'reader-type', null, null, -+ GObject.ParamFlags.READWRITE, -+ FingerprintReaderType.NONE, FingerprintReaderType.SWIPE, -+ FingerprintReaderType.NONE), -+ }; -+ -+ static [GObject.signals] = { -+ 'reader-type-changed': {}, -+ }; -+ -+ static { -+ GObject.registerClass(this); -+ } -+ -+ constructor() { -+ super(); -+ -+ this._fingerprintManagerProxy = 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, -+ }); -+ -+ this._initFingerprintManagerProxy(); -+ } -+ -+ get readerFound() { -+ return this.readerType !== FingerprintReaderType.NONE; -+ } -+ -+ setDefaultTimeout(timeout) { -+ this._fingerprintManagerProxy.set_default_timeout(timeout); -+ } -+ -+ async checkReaderType(cancellable) { -+ try { -+ // Wrappers don't support null cancellable, so let's ignore it in case -+ const args = cancellable ? [cancellable] : []; -+ const [devicePath] = -+ await this._fingerprintManagerProxy.GetDefaultDeviceAsync(...args); -+ -+ const fprintDeviceProxy = this._getFprintDeviceProxy(devicePath); -+ await fprintDeviceProxy.init_async(GLib.PRIORITY_DEFAULT, cancellable ?? null); -+ -+ const fingerprintReaderKey = fprintDeviceProxy['scan-type'].toUpperCase(); -+ const fingerprintReaderType = FingerprintReaderType[fingerprintReaderKey]; -+ this._setFingerprintReaderType(fingerprintReaderType); -+ } catch (e) { -+ if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) -+ return; -+ this._handleFingerprintError(e); -+ } -+ } -+ -+ async _initFingerprintManagerProxy() { -+ try { -+ await this._fingerprintManagerProxy.init_async( -+ GLib.PRIORITY_DEFAULT, null); -+ await this.checkReaderType(); -+ } catch (e) { -+ this._handleFingerprintError(e); -+ } -+ } -+ -+ _getFprintDeviceProxy(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, -+ }); -+ } -+ -+ _setFingerprintReaderType(fingerprintReaderType) { -+ if (fingerprintReaderType === undefined) -+ throw new Error(`Unexpected fingerprint device type '${fingerprintReaderType}'`); -+ -+ if (this.readerType === fingerprintReaderType) -+ return; -+ -+ this.readerType = fingerprintReaderType; -+ this.emit('reader-type-changed'); -+ } -+ -+ _handleFingerprintError(e) { -+ this._setFingerprintReaderType(FingerprintReaderType.NONE); -+ -+ if (e instanceof GLib.Error) { -+ if (e.matches(Gio.DBusError, Gio.DBusError.SERVICE_UNKNOWN)) -+ 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'); -+ } -+} +@@ -7,6 +7,7 @@ + gdm/batch.js + gdm/constants.js + gdm/credentialManager.js ++ gdm/fingerprintManager.js + gdm/loginDialog.js + gdm/oVirt.js + gdm/realmd.js -- -2.53.0 +2.54.0 -From 584b5621794d7aa445f46a366ca43847ac81779d Mon Sep 17 00:00:00 2001 +From 07865a6a750583ce50d66adbde51046a72aab73c Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Tue, 23 Sep 2025 16:45:59 +0200 -Subject: [PATCH 33/42] gdm/misc: Add PasskeyDeviceManager +Subject: [PATCH 43/54] 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. @@ -4674,42 +4905,18 @@ 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/dependencies.js | 1 + - js/misc/passkeyDeviceManager.js | 71 +++++++++++++++++++++++++++++++++ - 3 files changed, 73 insertions(+) - create mode 100644 js/misc/passkeyDeviceManager.js + js/gdm/passkeyDeviceManager.js | 69 ++++++++++++++++++++++++++++++++++ + js/js-resources.gresource.xml | 1 + + js/misc/dependencies.js | 1 + + 3 files changed, 71 insertions(+) + create mode 100644 js/gdm/passkeyDeviceManager.js -diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml -index 2712bfcc84..26b55b75bb 100644 ---- a/js/js-resources.gresource.xml -+++ b/js/js-resources.gresource.xml -@@ -40,6 +40,7 @@ - misc/objectManager.js - misc/params.js - misc/parentalControlsManager.js -+ misc/passkeyDeviceManager.js - misc/permissionStore.js - misc/signals.js - misc/signalTracker.js -diff --git a/js/misc/dependencies.js b/js/misc/dependencies.js -index f8f98d4871..c3c51aa2de 100644 ---- a/js/misc/dependencies.js -+++ b/js/misc/dependencies.js -@@ -18,6 +18,7 @@ import 'gi://GdkPixbuf?version=2.0'; - import 'gi://GnomeBG?version=4.0'; - import 'gi://GnomeDesktop?version=4.0'; - import 'gi://Graphene?version=1.0'; -+import 'gi://GUdev?version=1.0'; - import 'gi://GWeather?version=4.0'; - import 'gi://IBus?version=1.0'; - import 'gi://Pango?version=1.0'; -diff --git a/js/misc/passkeyDeviceManager.js b/js/misc/passkeyDeviceManager.js +diff --git a/js/gdm/passkeyDeviceManager.js b/js/gdm/passkeyDeviceManager.js new file mode 100644 -index 0000000000..9650ac1d2d +index 000000000..20799975a --- /dev/null -+++ b/js/misc/passkeyDeviceManager.js -@@ -0,0 +1,71 @@ ++++ b/js/gdm/passkeyDeviceManager.js +@@ -0,0 +1,69 @@ +import GObject from 'gi://GObject'; +import GUdev from 'gi://GUdev'; + @@ -4726,8 +4933,6 @@ index 0000000000..9650ac1d2d +} + +class PasskeyDeviceManager extends GObject.Object { -+ static [GObject.GTypeName] = 'PasskeyDeviceManager'; -+ + static [GObject.signals] = { + 'passkey-inserted': {param_types: [GObject.TYPE_JSOBJECT]}, + 'passkey-removed': {param_types: [GObject.TYPE_JSOBJECT]}, @@ -4781,21 +4986,172 @@ index 0000000000..9650ac1d2d + this.emit('passkey-removed', device); + } +} +diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml +index 50e0b179a..19daf6299 100644 +--- a/js/js-resources.gresource.xml ++++ b/js/js-resources.gresource.xml +@@ -10,6 +10,7 @@ + gdm/fingerprintManager.js + gdm/loginDialog.js + gdm/oVirt.js ++ gdm/passkeyDeviceManager.js + gdm/realmd.js + gdm/util.js + gdm/vmware.js +diff --git a/js/misc/dependencies.js b/js/misc/dependencies.js +index f8f98d487..c3c51aa2d 100644 +--- a/js/misc/dependencies.js ++++ b/js/misc/dependencies.js +@@ -18,6 +18,7 @@ import 'gi://GdkPixbuf?version=2.0'; + import 'gi://GnomeBG?version=4.0'; + import 'gi://GnomeDesktop?version=4.0'; + import 'gi://Graphene?version=1.0'; ++import 'gi://GUdev?version=1.0'; + import 'gi://GWeather?version=4.0'; + import 'gi://IBus?version=1.0'; + import 'gi://Pango?version=1.0'; -- -2.53.0 +2.54.0 -From 93c424c183dce14d4aa305fba71ccb2361a3b1dc Mon Sep 17 00:00:00 2001 +From 595c3331f83cc93b268b5afe25028f788c9d9522 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Tue, 19 May 2026 20:33:29 +0200 +Subject: [PATCH 44/54] gdm: Move smartcardManager.js to gdm + +This is a gdm component and was in misc. Move it here, where the +other *manager.js siblings are. +--- + js/{misc => gdm}/smartcardManager.js | 4 ++-- + js/gdm/util.js | 2 +- + js/js-resources.gresource.xml | 2 +- + js/ui/screenShield.js | 2 +- + 4 files changed, 5 insertions(+), 5 deletions(-) + rename js/{misc => gdm}/smartcardManager.js (97%) + +diff --git a/js/misc/smartcardManager.js b/js/gdm/smartcardManager.js +similarity index 97% +rename from js/misc/smartcardManager.js +rename to js/gdm/smartcardManager.js +index 51471e51d..21b28ea45 100644 +--- a/js/misc/smartcardManager.js ++++ b/js/gdm/smartcardManager.js +@@ -1,7 +1,7 @@ + import Gio from 'gi://Gio'; +-import * as Signals from './signals.js'; ++import * as Signals from '../misc/signals.js'; + +-import * as ObjectManager from './objectManager.js'; ++import * as ObjectManager from '../misc/objectManager.js'; + + const SmartcardTokenIface = ` + +diff --git a/js/gdm/util.js b/js/gdm/util.js +index a1c4ff939..db6488113 100644 +--- a/js/gdm/util.js ++++ b/js/gdm/util.js +@@ -12,7 +12,7 @@ import * as Vmware from './vmware.js'; + import * as Main from '../ui/main.js'; + import {logErrorUnlessCancelled} from '../misc/errorUtils.js'; + import * as Params from '../misc/params.js'; +-import * as SmartcardManager from '../misc/smartcardManager.js'; ++import * as SmartcardManager from './smartcardManager.js'; + + Gio._promisify(Gdm.Client.prototype, 'open_reauthentication_channel'); + Gio._promisify(Gdm.Client.prototype, 'get_user_verifier'); +diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml +index 19daf6299..162e19d41 100644 +--- a/js/js-resources.gresource.xml ++++ b/js/js-resources.gresource.xml +@@ -12,6 +12,7 @@ + gdm/oVirt.js + gdm/passkeyDeviceManager.js + gdm/realmd.js ++ gdm/smartcardManager.js + gdm/util.js + gdm/vmware.js + +@@ -44,7 +45,6 @@ + misc/permissionStore.js + misc/signals.js + misc/signalTracker.js +- misc/smartcardManager.js + misc/systemActions.js + misc/timeLimitsManager.js + misc/util.js +diff --git a/js/ui/screenShield.js b/js/ui/screenShield.js +index 44b871502..e42b2974a 100644 +--- a/js/ui/screenShield.js ++++ b/js/ui/screenShield.js +@@ -17,7 +17,7 @@ import * as Main from './main.js'; + import * as Overview from './overview.js'; + import * as MessageTray from './messageTray.js'; + import * as ShellDBus from './shellDBus.js'; +-import * as SmartcardManager from '../misc/smartcardManager.js'; ++import * as SmartcardManager from '../gdm/smartcardManager.js'; + + import {adjustAnimationTime} from '../misc/animationUtils.js'; + +-- +2.54.0 + + +From 38399275c7c5b5144d0a381207b6b80e5c0b6e08 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Tue, 10 Mar 2026 14:27:50 +0100 +Subject: [PATCH 45/54] gdm/smartcardManager: Skip login_token aliases to avoid + duplicate events + +Tokens with '/login_token' paths are aliases mirroring already +connected tokens. Connecting to both causes duplicate events. +--- + js/gdm/smartcardManager.js | 6 ++++++ + 1 file changed, 6 insertions(+) + +diff --git a/js/gdm/smartcardManager.js b/js/gdm/smartcardManager.js +index 21b28ea45..98f762d7d 100644 +--- a/js/gdm/smartcardManager.js ++++ b/js/gdm/smartcardManager.js +@@ -70,6 +70,12 @@ class SmartcardManager extends Signals.EventEmitter { + } + + _addToken(token) { ++ // This is an alias, mirroring the real token. ++ // Skip it to avoid duplicate events ++ const objectPath = token.get_object_path(); ++ if (objectPath.endsWith('/login_token')) ++ return; ++ + this._updateToken(token); + + token.connectObject('g-properties-changed', (proxy, properties) => { +-- +2.54.0 + + +From 2cc656533a3894a50daea881e3d896c8aaf951df Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Mon, 18 Aug 2025 12:08:21 +0200 -Subject: [PATCH 34/42] gdm: Add AuthServices +Subject: [PATCH 46/54] gdm: Add AuthServices MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -All authentication mechanisms that were handled in util.js UserVerifier -are ported to a new class called AuthServices. In util.js it's left -getting the userVerifier proxies. +util.js had become overly complex, handling too many decoupled responsibilities. +To improve readability and maintainability, it's been introduced a dedicated +class to manage authentication services and their respective logic that +was previously in util.js. + +AuthServices is the base class that defines the common interface (signals +like "ask-question", "verification-complete", "mechanisms-changed", etc.) and +shared logic for authentication handling, while its childs like AuthServicesLegacy +are derived implementations that handle different authentication backends. + +The Legacy naming in AuthServicesLegacy distinguishes the traditional PAM-based +architecture (rigid PAM messages and different services) from the upcoming +unified approach (JSON PAM messages and unified single service). It is important +to note that it's not deprecated; it remains the primary handler for standard GDM/PAM +services (gdm-password, gdm-smartcard, gdm-fingerprint). The default workflow of authServices is: 1. beginVerification method is called with userName (optional) and @@ -4815,7 +5171,7 @@ The default workflow of authServices is: The main properties of authServices are: * _enabledRoles: Contains the roles that are currently enabled in GDM settings. - Valid roles are defined in const.js (PASSWORD_ROLE_NAME, SMARTCARD_ROLE_NAME...). + Valid roles are defined in constants.js (PASSWORD_ROLE_NAME, SMARTCARD_ROLE_NAME...). This determines which mechanisms are included in _enabledMechanisms. * _enabledMechanisms: Contains the mechanisms that are currently enabled. These are the ones sent on 'mechanisms-changed'. @@ -4828,7 +5184,7 @@ The main properties of authServices are: list, etc. and gets the responses. The authentication of password, smartcard and fingerprint now is -implemented in a child class of AuthServices called AuthServicesLegacy. +implemented in the child class called AuthServicesLegacy. 1. It implements a hardcoded array of mechanisms based on roles (password, smartcard and fingerprint). 2. Each mechanism is driven by a different PAM service, and _roleToService @@ -4846,30 +5202,21 @@ Based on the previous work done by: - Marco Trevisan (Treviño) - Ray Strode --- - js/gdm/authPrompt.js | 44 +-- - js/gdm/authServices.js | 476 ++++++++++++++++++++++ - js/gdm/authServicesLegacy.js | 324 +++++++++++++++ - js/gdm/util.js | 718 +++++++--------------------------- + js/gdm/authPrompt.js | 57 +-- + js/gdm/authServices.js | 473 ++++++++++++++++++++++ + js/gdm/authServicesLegacy.js | 334 ++++++++++++++++ + js/gdm/util.js | 731 ++++++++-------------------------- js/js-resources.gresource.xml | 2 + - js/misc/fingerprintManager.js | 4 - po/POTFILES.in | 1 + - 7 files changed, 961 insertions(+), 608 deletions(-) + 6 files changed, 983 insertions(+), 615 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 9a9c0c3802..d8c7c6409d 100644 +index ff51c05f3..db4c1647a 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -10,7 +10,6 @@ import St from 'gi://St'; - import * as Animation from '../ui/animation.js'; - import * as AuthList from './authList.js'; - import * as Batch from './batch.js'; --import * as Const from './const.js'; - import * as GdmUtil from './util.js'; - import * as Params from '../misc/params.js'; - import * as ShellEntry from '../ui/shellEntry.js'; -@@ -92,10 +91,7 @@ export const AuthPrompt = GObject.registerClass({ +@@ -91,10 +91,7 @@ export const AuthPrompt = GObject.registerClass({ 'verification-failed', this._onVerificationFailed.bind(this), 'verification-complete', this._onVerificationComplete.bind(this), 'reset', this._onReset.bind(this), @@ -4880,7 +5227,7 @@ index 9a9c0c3802..d8c7c6409d 100644 this.connect('destroy', this._onDestroy.bind(this)); -@@ -445,31 +441,6 @@ export const AuthPrompt = GObject.registerClass({ +@@ -439,46 +436,8 @@ export const AuthPrompt = GObject.registerClass({ this.emit('prompted'); } @@ -4899,7 +5246,7 @@ index 9a9c0c3802..d8c7c6409d 100644 - // with a smartcard - // 2) Don't reset if we've already succeeded at verification and - // the user is getting logged in. -- if (this._userVerifier.serviceIsDefault(Const.SMARTCARD_SERVICE_NAME) && +- if (this._userVerifier.serviceIsDefault(Constants.SMARTCARD_SERVICE_NAME) && - (this.verificationStatus === AuthPromptStatus.VERIFYING || - this.verificationStatus === AuthPromptStatus.VERIFICATION_IN_PROGRESS) && - this.smartcardDetected) @@ -4909,24 +5256,38 @@ index 9a9c0c3802..d8c7c6409d 100644 - this.reset(); - } - - _onShowMessage(_userVerifier, serviceName, message, type) { - let wiggleParameters = {duration: 0}; +- _onShowMessage(_userVerifier, serviceName, message, type) { +- let wiggleParameters = {duration: 0}; +- +- if (type === GdmUtil.MessageType.ERROR && +- this._userVerifier.serviceIsFingerprint(serviceName)) { +- // TODO: Use Await for wiggle to be over before unfreezing the user verifier queue +- wiggleParameters = { +- duration: 65, +- wiggleCount: 3, +- }; +- this._userVerifier.increaseCurrentMessageTimeout( +- wiggleParameters.duration * (wiggleParameters.wiggleCount + 2)); +- } +- +- this.setMessage(message, type, wiggleParameters); ++ _onShowMessage(_userVerifier, serviceName, message, type, showMessageResolver) { ++ this.setMessage(message, type); + this.emit('prompted'); -@@ -782,6 +753,13 @@ export const AuthPrompt = GObject.registerClass({ - if (invalidStatus.includes(this.verificationStatus)) - return false; + // If we're showing a message and no auth widget is currently visible, +@@ -496,7 +455,9 @@ export const AuthPrompt = GObject.registerClass({ + if (wasQueryingService) + this._queryingService = null; -+ const oldPromptStep = this._promptStep; -+ this._promptStep = 0; -+ if (!this._userVerifier.selectMechanism(mechanism)) -+ this._promptStep = oldPromptStep; -+ -+ this._updateCancelButton(); -+ - return true; - } - -@@ -803,8 +781,10 @@ export const AuthPrompt = GObject.registerClass({ +- 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._entry.text = ''; + this.updateSensitivity({sensitive: true}); + } +@@ -809,8 +770,10 @@ export const AuthPrompt = GObject.registerClass({ this._preemptiveAnswerWatchId = this._idleMonitor.add_idle_watch(3000, this._onUserStoppedTypePreemptiveAnswer.bind(this)); @@ -4937,9 +5298,9 @@ index 9a9c0c3802..d8c7c6409d 100644 + else + this._userVerifier?.reset(); - reuseEntryText = reuseEntryText || this._preemptiveInput; + reuseEntryText = reuseEntryText || !!this._preemptiveAnswer || this._preemptiveInput; -@@ -828,7 +808,7 @@ export const AuthPrompt = GObject.registerClass({ +@@ -833,7 +796,7 @@ export const AuthPrompt = GObject.registerClass({ if (oldStatus === AuthPromptStatus.VERIFICATION_CANCELLED) return; resetType = ResetType.PROVIDE_USERNAME; @@ -4950,15 +5311,17 @@ index 9a9c0c3802..d8c7c6409d 100644 resetType = ResetType.DONT_PROVIDE_USERNAME; diff --git a/js/gdm/authServices.js b/js/gdm/authServices.js new file mode 100644 -index 0000000000..f702b15a04 +index 000000000..8f05a5d8f --- /dev/null +++ b/js/gdm/authServices.js -@@ -0,0 +1,476 @@ +@@ -0,0 +1,473 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + -+import * as FingerprintManager from '../misc/fingerprintManager.js'; ++import * as Constants from './constants.js'; ++import * as FingerprintManager from './fingerprintManager.js'; +import * as Params from '../misc/params.js'; -+import * as SmartcardManager from '../misc/smartcardManager.js'; ++import {registerDestroyableType} from '../misc/signalTracker.js'; ++import * as SmartcardManager from './smartcardManager.js'; +import {logErrorUnlessCancelled} from '../misc/errorUtils.js'; +import * as Util from './util.js'; +import Gdm from 'gi://Gdm'; @@ -4970,11 +5333,12 @@ index 0000000000..f702b15a04 +Gio._promisify(Gdm.Client.prototype, 'get_user_verifier'); +Gio._promisify(Gdm.UserVerifierProxy.prototype, 'call_begin_verification_for_user'); +Gio._promisify(Gdm.UserVerifierProxy.prototype, 'call_begin_verification'); ++Gio._promisify(Gdm.UserVerifierProxy.prototype, 'call_answer_query'); ++Gio._promisify(Gdm.UserVerifierChoiceListProxy.prototype, 'call_select_choice'); + +export class AuthServices extends GObject.Object { -+ static [GObject.GTypeName] = 'AuthServices'; -+ + static [GObject.signals] = { ++ 'destroy': {}, + 'queue-message': { + param_types: [GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_UINT], + }, @@ -5005,6 +5369,7 @@ index 0000000000..f702b15a04 + + static { + GObject.registerClass(this); ++ registerDestroyableType(this); + } + + static SupportedRoles = []; @@ -5034,8 +5399,12 @@ index 0000000000..f702b15a04 + + 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() { @@ -5058,13 +5427,8 @@ index 0000000000..f702b15a04 + this._handleSelectChoice(serviceName, key); + } + -+ async answerQuery(serviceName, answer) { -+ try { -+ await this._waitPendingMessages(); -+ await this._handleAnswerQuery(serviceName, answer); -+ } catch (e) { -+ logErrorUnlessCancelled(e); -+ } ++ answerQuery(serviceName, answer) { ++ this._handleAnswerQuery(serviceName, answer); + } + + async beginVerification(userName, userVerifierProxies) { @@ -5115,6 +5479,12 @@ index 0000000000..f702b15a04 + this._handleCancel(); + } + ++ destroy() { ++ this.reset(); ++ this.clear(); ++ this.emit('destroy'); ++ } ++ + clear() { + this._cancellable?.cancel(); + this._cancellable = null; @@ -5172,38 +5542,26 @@ index 0000000000..f702b15a04 + + _waitPendingMessages() { + const cancellable = this._cancellable; -+ return new Promise((resolve, reject) => { -+ let done = false; -+ const safeResolve = () => { -+ if (!done) { -+ done = true; -+ if (cancellable?.is_cancelled()) { -+ reject(new GLib.Error( -+ Gio.IOErrorEnum, -+ Gio.IOErrorEnum.CANCELLED, -+ 'Operation was cancelled')); -+ } else { -+ resolve(); -+ } -+ } -+ }; -+ const safeReject = err => { -+ if (!done) { -+ done = true; -+ reject(err); -+ } -+ }; -+ const waiter = { -+ resolve: safeResolve, -+ reject: safeReject, -+ }; -+ this.emit('wait-pending-messages', waiter); ++ const timeoutId = GLib.timeout_add_seconds_once(GLib.PRIORITY_DEFAULT, 10, ++ () => cancellable.cancel()); + -+ GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 10, () => { -+ safeReject(new Error('Timed out waiting for pending messages')); -+ return GLib.SOURCE_REMOVE; -+ }); ++ const {promise, resolve, reject} = Promise.withResolvers(); ++ const task = Gio.Task.new(this, cancellable, () => { ++ try { ++ const res = task.propagate_boolean(); ++ if (!res) ++ throw new GLib.Error(Gio.IOErrorEnum, Gio.IOErrorEnum.FAILED, 'Operation failed'); ++ resolve(); ++ } catch (e) { ++ reject(e); ++ } finally { ++ GLib.source_remove(timeoutId); ++ } + }); ++ ++ this.emit('wait-pending-messages', task); ++ ++ return promise; + } + + _updateUserVerifier(proxies) { @@ -5374,7 +5732,7 @@ index 0000000000..f702b15a04 + + _handleSelectChoice() {} + -+ async _handleAnswerQuery() {} ++ _handleAnswerQuery() {} + + _handleBeginVerification() {} + @@ -5432,15 +5790,16 @@ index 0000000000..f702b15a04 +} diff --git a/js/gdm/authServicesLegacy.js b/js/gdm/authServicesLegacy.js new file mode 100644 -index 0000000000..e9c1676ab9 +index 000000000..a77c0fbc0 --- /dev/null +++ b/js/gdm/authServicesLegacy.js -@@ -0,0 +1,324 @@ +@@ -0,0 +1,334 @@ +import GLib from 'gi://GLib'; +import GObject from 'gi://GObject'; + -+import * as Const from './const.js'; -+import {FingerprintReaderType} from '../misc/fingerprintManager.js'; ++import * as Constants from './constants.js'; ++import {FingerprintReaderType} from './fingerprintManager.js'; ++import {logErrorUnlessCancelled} from '../misc/errorUtils.js'; +import * as OVirt from './oVirt.js'; +import * as Util from './util.js'; +import * as Vmware from './vmware.js'; @@ -5450,35 +5809,33 @@ index 0000000000..e9c1676ab9 + +const Mechanisms = [ + { -+ serviceName: Const.PASSWORD_SERVICE_NAME, -+ role: Const.PASSWORD_ROLE_NAME, -+ name: 'Password', ++ serviceName: Constants.PASSWORD_SERVICE_NAME, ++ role: Constants.PASSWORD_ROLE_NAME, ++ name: _('Password'), + }, + { -+ serviceName: Const.SMARTCARD_SERVICE_NAME, -+ role: Const.SMARTCARD_ROLE_NAME, -+ name: 'Smartcard', ++ serviceName: Constants.SMARTCARD_SERVICE_NAME, ++ role: Constants.SMARTCARD_ROLE_NAME, ++ name: _('Smartcard'), + }, + { -+ serviceName: Const.FINGERPRINT_SERVICE_NAME, -+ role: Const.FINGERPRINT_ROLE_NAME, -+ name: 'Fingerprint', ++ serviceName: Constants.FINGERPRINT_SERVICE_NAME, ++ role: Constants.FINGERPRINT_ROLE_NAME, ++ name: _('Fingerprint'), + }, +]; + +export class AuthServicesLegacy extends AuthServices { -+ static [GObject.GTypeName] = 'AuthServicesLegacy'; -+ + static SupportedRoles = [ -+ Const.PASSWORD_ROLE_NAME, -+ Const.SMARTCARD_ROLE_NAME, -+ Const.FINGERPRINT_ROLE_NAME, ++ Constants.PASSWORD_ROLE_NAME, ++ Constants.SMARTCARD_ROLE_NAME, ++ Constants.FINGERPRINT_ROLE_NAME, + ]; + + static RoleToService = { -+ [Const.PASSWORD_ROLE_NAME]: Const.PASSWORD_SERVICE_NAME, -+ [Const.SMARTCARD_ROLE_NAME]: Const.SMARTCARD_SERVICE_NAME, -+ [Const.FINGERPRINT_ROLE_NAME]: Const.FINGERPRINT_SERVICE_NAME, ++ [Constants.PASSWORD_ROLE_NAME]: Constants.PASSWORD_SERVICE_NAME, ++ [Constants.SMARTCARD_ROLE_NAME]: Constants.SMARTCARD_SERVICE_NAME, ++ [Constants.FINGERPRINT_ROLE_NAME]: Constants.FINGERPRINT_SERVICE_NAME, + }; + + static { @@ -5491,8 +5848,8 @@ index 0000000000..e9c1676ab9 + this._updateEnabledMechanisms(); + + this._credentialManagers = {}; -+ this._addCredentialManager(OVirt.SERVICE_NAME, OVirt.getOVirtCredentialsManager()); -+ this._addCredentialManager(Vmware.SERVICE_NAME, Vmware.getVmwareCredentialsManager()); ++ this.addCredentialManager(OVirt.SERVICE_NAME, OVirt.getOVirtCredentialsManager()); ++ this.addCredentialManager(Vmware.SERVICE_NAME, Vmware.getVmwareCredentialsManager()); + } + + _handleSelectChoice(serviceName, key) { @@ -5500,20 +5857,18 @@ index 0000000000..e9c1676ab9 + return; + + this._userVerifierChoiceList.call_select_choice( -+ serviceName, key, this._cancellable, null); ++ serviceName, key, this._cancellable).catch(logErrorUnlessCancelled); + } + -+ async _handleAnswerQuery(serviceName, answer) { ++ _handleAnswerQuery(serviceName, answer) { + if (serviceName !== this._selectedMechanism?.serviceName) + return; + -+ if (this._selectedMechanism.role === Const.SMARTCARD_ROLE_NAME) ++ if (this._selectedMechanism.role === Constants.SMARTCARD_ROLE_NAME) + this._smartcardInProgress = true; + -+ await this._userVerifier.call_answer_query(serviceName, -+ answer, -+ this._cancellable, -+ null); ++ this._userVerifier.call_answer_query( ++ serviceName, answer, this._cancellable).catch(logErrorUnlessCancelled); + } + + _handleBeginVerification() { @@ -5531,7 +5886,7 @@ index 0000000000..e9c1676ab9 + // 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 === Const.SMARTCARD_ROLE_NAME || ++ this._enabledMechanisms[0].role === Constants.SMARTCARD_ROLE_NAME || + Object.keys(this._credentialManagers).includes(this._selectedMechanism?.serviceName)); + } + @@ -5547,7 +5902,7 @@ index 0000000000..e9c1676ab9 + if (!this._fingerprintManager?.readerFound) { + this._enabledMechanisms.push(...Mechanisms.filter(m => + this._enabledRoles.includes(m.role) && -+ m.role !== Const.FINGERPRINT_ROLE_NAME ++ m.role !== Constants.FINGERPRINT_ROLE_NAME + )); + } else { + this._enabledMechanisms.push(...Mechanisms.filter(m => @@ -5557,7 +5912,7 @@ index 0000000000..e9c1676ab9 + } + + _handleSmartcardChanged() { -+ if (this._selectedMechanism?.role !== Const.SMARTCARD_ROLE_NAME || ++ if (this._selectedMechanism?.role !== Constants.SMARTCARD_ROLE_NAME || + this._smartcardInProgress && this._smartcardManager.hasInsertedTokens()) + return; + @@ -5565,7 +5920,7 @@ index 0000000000..e9c1676ab9 + } + + _handleFingerprintChanged() { -+ if (!this._enabledRoles.includes(Const.FINGERPRINT_ROLE_NAME)) ++ if (!this._enabledRoles.includes(Constants.FINGERPRINT_ROLE_NAME)) + return; + + this._updateEnabledMechanisms(); @@ -5575,7 +5930,7 @@ index 0000000000..e9c1676ab9 + _handleOnInfo(serviceName, info) { + if (serviceName === this._selectedMechanism?.serviceName) { + this.emit('queue-message', serviceName, info, Util.MessageType.INFO); -+ } else if (serviceName === Const.FINGERPRINT_SERVICE_NAME && ++ } 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 @@ -5595,7 +5950,7 @@ index 0000000000..e9c1676ab9 + + _handleOnProblem(serviceName, problem) { + if (serviceName === this._selectedMechanism?.serviceName || -+ (serviceName === Const.FINGERPRINT_SERVICE_NAME && ++ (serviceName === Constants.FINGERPRINT_SERVICE_NAME && + this._enabledMechanisms.some(m => m.serviceName === serviceName))) { + this.emit('queue-priority-message', + serviceName, @@ -5603,7 +5958,7 @@ index 0000000000..e9c1676ab9 + Util.MessageType.ERROR); + } + -+ if (serviceName === Const.FINGERPRINT_SERVICE_NAME && ++ if (serviceName === Constants.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. @@ -5657,7 +6012,7 @@ index 0000000000..e9c1676ab9 + + _handleOnConversationStopped(serviceName) { + if (serviceName !== this._selectedMechanism?.serviceName && -+ serviceName !== Const.FINGERPRINT_SERVICE_NAME) ++ serviceName !== Constants.FINGERPRINT_SERVICE_NAME) + return; + + // If the login failed with the preauthenticated oVirt credentials @@ -5670,7 +6025,7 @@ index 0000000000..e9c1676ab9 + return; + } + -+ if (serviceName === Const.FINGERPRINT_SERVICE_NAME && ++ if (serviceName === Constants.FINGERPRINT_SERVICE_NAME && + this._unavailableServices.has(serviceName)) { + this._enabledMechanisms = this._enabledMechanisms + .filter(m => m.serviceName !== serviceName); @@ -5690,18 +6045,19 @@ index 0000000000..e9c1676ab9 + } + + _handleOnServiceUnavailable(serviceName, errorMessage) { -+ if (serviceName === Const.FINGERPRINT_SERVICE_NAME && -+ this._enabledMechanisms.some(m => m.serviceName === serviceName) && -+ errorMessage) { -+ this.emit('queue-message', -+ serviceName, -+ errorMessage, -+ Util.MessageType.ERROR); -+ } ++ if (serviceName !== Constants.FINGERPRINT_SERVICE_NAME || ++ !this._enabledMechanisms.some(m => m.serviceName === serviceName) || ++ !errorMessage) ++ return; ++ ++ this.emit('queue-message', ++ serviceName, ++ errorMessage, ++ Util.MessageType.ERROR); + } + + _handleVerificationFailed(serviceName) { -+ if (serviceName === Const.FINGERPRINT_SERVICE_NAME && ++ if (serviceName === Constants.FINGERPRINT_SERVICE_NAME && + this._enabledMechanisms.some(m => m.serviceName === serviceName) && + this._fingerprintFailedId) + GLib.source_remove(this._fingerprintFailedId); @@ -5734,12 +6090,12 @@ index 0000000000..e9c1676ab9 + + _handleCanStartService(serviceName) { + return serviceName === this._selectedMechanism?.serviceName || -+ (serviceName === Const.FINGERPRINT_SERVICE_NAME && ++ (serviceName === Constants.FINGERPRINT_SERVICE_NAME && + this._enabledMechanisms.some(m => m.serviceName === serviceName) && + this._userName); + } + -+ _addCredentialManager(serviceName, credentialManager) { ++ addCredentialManager(serviceName, credentialManager) { + if (this._credentialManagers[serviceName]) + return; + @@ -5752,19 +6108,31 @@ index 0000000000..e9c1676ab9 + this); + } + ++ removeCredentialManager(serviceName) { ++ const credentialManager = this._credentialManagers[serviceName]; ++ if (!credentialManager) ++ return; ++ ++ credentialManager.disconnectObject(this); ++ delete this._credentialManagers[serviceName]; ++ ++ if (this._selectedMechanism?.serviceName === serviceName) ++ this.emit('reset'); ++ } ++ + _onCredentialManagerAuthenticated(credentialManager) { + this._selectedMechanism = { + serviceName: credentialManager.service, -+ role: Const.PASSWORD_ROLE_NAME, ++ role: Constants.PASSWORD_ROLE_NAME, + }; + this.emit('reset', {softReset: true}); + } +} diff --git a/js/gdm/util.js b/js/gdm/util.js -index ae464665e2..8c11c63c49 100644 +index db6488113..ddfecd0cf 100644 --- a/js/gdm/util.js +++ b/js/gdm/util.js -@@ -1,25 +1,13 @@ +@@ -1,24 +1,14 @@ import Clutter from 'gi://Clutter'; -import Gdm from 'gi://Gdm'; import Gio from 'gi://Gio'; @@ -5772,15 +6140,14 @@ index ae464665e2..8c11c63c49 100644 import * as Signals from '../misc/signals.js'; import * as Batch from './batch.js'; - import * as Const from './const.js'; --import * as FingerprintManager from '../misc/fingerprintManager.js'; + import * as Constants from './constants.js'; +-import {FingerprintManager, FingerprintReaderType} from './fingerprintManager.js'; -import * as OVirt from './oVirt.js'; -import * as Vmware from './vmware.js'; import * as Main from '../ui/main.js'; --import {logErrorUnlessCancelled} from '../misc/errorUtils.js'; --import {loadInterfaceXML} from '../misc/fileUtils.js'; + import {logErrorUnlessCancelled} from '../misc/errorUtils.js'; import * as Params from '../misc/params.js'; --import * as SmartcardManager from '../misc/smartcardManager.js'; +-import * as SmartcardManager from './smartcardManager.js'; - -Gio._promisify(Gdm.Client.prototype, 'open_reauthentication_channel'); -Gio._promisify(Gdm.Client.prototype, 'get_user_verifier'); @@ -5791,9 +6158,9 @@ index ae464665e2..8c11c63c49 100644 const CLONE_FADE_ANIMATION_TIME = 250; -@@ -42,9 +30,6 @@ const USER_READ_TIME = 48; - const USER_READ_TIME_MIN = 2000; - const MESSAGE_TIME_MULTIPLIER = GLib.getenv('GDM_MESSAGE_TIME_MULTIPLIER') ?? 1; +@@ -44,9 +34,6 @@ const MESSAGE_TIME_MULTIPLIER = (() => { + return Number.isFinite(value) && value > 0 ? value : 1; + })(); -const FINGERPRINT_SERVICE_PROXY_TIMEOUT = 5000; -const FINGERPRINT_ERROR_TIMEOUT_WAIT = 15; @@ -5801,15 +6168,20 @@ index ae464665e2..8c11c63c49 100644 /** * Keep messages in order by priority * -@@ -57,6 +42,15 @@ export const MessageType = { +@@ -59,6 +46,20 @@ export const MessageType = { ERROR: 3, }; ++/** ++ * 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); -+ this.error = error; -+ this.where = message; ++ super(message, {cause: error}); + this.serviceName = serviceName; + } +} @@ -5817,13 +6189,13 @@ index ae464665e2..8c11c63c49 100644 /** * @param {Clutter.Actor} actor */ -@@ -130,53 +124,12 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -135,53 +136,12 @@ export class ShellUserVerifier extends Signals.EventEmitter { this._client = client; this._cancellable = null; - this._defaultService = null; - this._preemptingService = null; -- this._fingerprintReaderType = FingerprintManager.FingerprintReaderType.NONE; +- this._fingerprintReaderType = FingerprintReaderType.NONE; - this._fingerprintReaderFound = false; - this._messageQueue = []; @@ -5872,7 +6244,7 @@ index ae464665e2..8c11c63c49 100644 } get hasPendingMessages() { -@@ -191,79 +144,72 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -196,84 +156,85 @@ export class ShellUserVerifier extends Signals.EventEmitter { return this._messageQueue ? this._messageQueue[0] : null; } @@ -5903,19 +6275,29 @@ index ae464665e2..8c11c63c49 100644 + hold?.release(); } + selectMechanism(mechanism) { +- // TODO: Implement mechanism selection +- return false; ++ return this._authServicesLegacy?.selectMechanism(mechanism); + } + - cancel() { - if (this._cancellable) - this._cancellable.cancel(); -+ selectMechanism(mechanism) { -+ return this._authServicesLegacy?.selectMechanism(mechanism); ++ needsUsername() { ++ return this._authServicesLegacy?.needsUsername(); + } - if (this._userVerifier) { - this._userVerifier.call_cancel_sync(null); - this.clear(); - } -+ needsUsername() { -+ return this._authServicesLegacy?.needsUsername(); ++ reset() { ++ this._authServicesLegacy?.reset(); ++ ++ this._userVerifier?.call_cancel_sync(null); ++ ++ this.clear(); } - _clearUserVerifier() { @@ -5925,14 +6307,6 @@ index ae464665e2..8c11c63c49 100644 - this._userVerifier = null; - this._userVerifierChoiceList = null; - } -+ reset() { -+ this._authServicesLegacy?.reset(); -+ -+ this._userVerifier?.call_cancel_sync(null); -+ -+ this.clear(); -+ } -+ + cancel() { + this._authServicesLegacy?.cancel(); + @@ -5959,26 +6333,37 @@ index ae464665e2..8c11c63c49 100644 } destroy() { -+ this._authServicesLegacy?.disconnectObject(this); ++ this._authServicesLegacy?.destroy(); ++ this._authServicesLegacy = null; + this.cancel(); this._settings.run_dispose(); this._settings = null; -- ++ } + - this._smartcardManager?.disconnectObject(this); - this._smartcardManager = null; -- ++ selectChoice(serviceName, key) { ++ this._authServicesLegacy?.selectChoice(serviceName, key); ++ } + - this._fingerprintManager?.disconnectObject(this); - this._fingerprintManager = null; -- ++ async answerQuery(serviceName, answer) { ++ // Wait for pending messages to be displayed before answering to ++ // ensure no messages get lost ++ await this._handlePendingMessages().catch(logErrorUnlessCancelled); + - for (let service in this._credentialManagers) - this.removeCredentialManager(service); ++ this._authServicesLegacy?.answerQuery(serviceName, answer); } - selectChoice(serviceName, key) { +- selectChoice(serviceName, key) { - this._userVerifierChoiceList.call_select_choice(serviceName, key, this._cancellable, null); -+ this._authServicesLegacy?.selectChoice(serviceName, key); ++ addCredentialManager(serviceName, credentialManager) { ++ this._authServicesLegacy?.addCredentialManager(serviceName, credentialManager); } - async answerQuery(serviceName, answer) { @@ -5988,13 +6373,13 @@ index ae464665e2..8c11c63c49 100644 - } catch (e) { - logErrorUnlessCancelled(e); - } -+ answerQuery(serviceName, answer) { -+ this._authServicesLegacy?.answerQuery(serviceName, answer); ++ removeCredentialManager(serviceName) { ++ this._authServicesLegacy?.removeCredentialManager(serviceName); } _getIntervalForMessage(message) { -@@ -275,7 +221,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { - USER_READ_TIME_MIN); +@@ -285,7 +246,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { + MESSAGE_TIME_MULTIPLIER; } - finishMessageQueue() { @@ -6002,7 +6387,7 @@ index ae464665e2..8c11c63c49 100644 if (!this.hasPendingMessages) return; -@@ -318,7 +264,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -328,7 +289,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { this._messageQueue.shift(); this._queueMessageTimeout(); } else { @@ -6011,7 +6396,7 @@ index ae464665e2..8c11c63c49 100644 } return GLib.SOURCE_REMOVE; -@@ -348,7 +294,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -358,7 +319,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { } _clearMessageQueue() { @@ -6020,7 +6405,7 @@ index ae464665e2..8c11c63c49 100644 if (this._messageQueueTimeoutId !== 0) { GLib.source_remove(this._messageQueueTimeoutId); -@@ -357,471 +303,142 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -367,470 +328,147 @@ export class ShellUserVerifier extends Signals.EventEmitter { this.emit('show-message', null, null, MessageType.NONE); } @@ -6028,8 +6413,7 @@ index ae464665e2..8c11c63c49 100644 - if (this._fingerprintManager) - return; - -- this._fingerprintManager = -- new FingerprintManager.FingerprintManager(this._cancellable); +- this._fingerprintManager = new FingerprintManager(this._cancellable); - this._fingerprintManager.connectObject( - 'reader-type-changed', () => this._onFingerprintReaderTypeChanged(), - this); @@ -6057,7 +6441,7 @@ index ae464665e2..8c11c63c49 100644 - this._updateDefaultService(); - - if (this._userVerifier && -- !this._activeServices.has(Const.FINGERPRINT_SERVICE_NAME)) +- !this._activeServices.has(Constants.FINGERPRINT_SERVICE_NAME)) - this._maybeStartFingerprintVerification(); - } - @@ -6088,9 +6472,7 @@ index ae464665e2..8c11c63c49 100644 - - _checkForSmartcard() { - let smartcardDetected; -+ _reportInitError(initError) { -+ const {error, where, serviceName} = initError; - +- - if (!this._settings.get_boolean(SMARTCARD_AUTHENTICATION_KEY)) - smartcardDetected = false; - else if (this._reauthOnly) @@ -6102,19 +6484,22 @@ index ae464665e2..8c11c63c49 100644 - this.smartcardDetected = smartcardDetected; - - if (this.smartcardDetected) -- this._preemptingService = Const.SMARTCARD_SERVICE_NAME; -- else if (this._preemptingService === Const.SMARTCARD_SERVICE_NAME) +- this._preemptingService = Constants.SMARTCARD_SERVICE_NAME; +- else if (this._preemptingService === Constants.SMARTCARD_SERVICE_NAME) - this._preemptingService = null; - - this._updateDefaultService(); -- ++ _reportInitError(initError) { ++ const {cause, message, serviceName} = initError; + - this.emit('smartcard-status-changed'); - } - } - - _reportInitError(where, error, serviceName) { - logError(error, where); +- logError(error, where); - this._hold?.release(); ++ logError(cause, message); this._queueMessage(serviceName, _('Authentication error'), MessageType.ERROR); - this._failCounter++; @@ -6198,9 +6583,8 @@ index ae464665e2..8c11c63c49 100644 - return; - this._reportInitError('Failed to obtain user verifier', e); - return; -+ throw new InitError(e, 'Failed to obtain user verifier extensions'); - } - +- } +- - if (this._client.get_user_verifier_choice_list) - this._userVerifierChoiceList = this._client.get_user_verifier_choice_list(); - else @@ -6252,9 +6636,10 @@ index ae464665e2..8c11c63c49 100644 - for (let serviceName in this._credentialManagers) { - if (this.serviceIsForeground(serviceName)) - return true; -- } -- -- return this.serviceIsForeground(Const.SMARTCARD_SERVICE_NAME); ++ throw new InitError(e, 'Failed to obtain user verifier extensions'); + } + +- return this.serviceIsForeground(Constants.SMARTCARD_SERVICE_NAME); - } - - serviceIsDefault(serviceName) { @@ -6264,8 +6649,8 @@ index ae464665e2..8c11c63c49 100644 serviceIsFingerprint(serviceName) { - return this._fingerprintReaderFound && -- serviceName === Const.FINGERPRINT_SERVICE_NAME; -+ return serviceName === Const.FINGERPRINT_SERVICE_NAME; +- serviceName === Constants.FINGERPRINT_SERVICE_NAME; ++ return serviceName === Constants.FINGERPRINT_SERVICE_NAME; } _onSettingsChanged() { @@ -6284,7 +6669,7 @@ index ae464665e2..8c11c63c49 100644 - this._fingerprintReaderFound = false; - this._fingerprintReaderType = FingerprintReaderType.NONE; - -- if (this._activeServices.has(Const.FINGERPRINT_SERVICE_NAME)) +- if (this._activeServices.has(Constants.FINGERPRINT_SERVICE_NAME)) - needsReset = true; - } - @@ -6294,50 +6679,50 @@ index ae464665e2..8c11c63c49 100644 - this._smartcardManager.disconnectObject(this); - this._smartcardManager = null; - -- if (this._activeServices.has(Const.SMARTCARD_SERVICE_NAME)) +- if (this._activeServices.has(Constants.SMARTCARD_SERVICE_NAME)) - needsReset = true; - } - - if (needsReset) - this._cancelAndReset(); -- } -- -- _getDetectedDefaultService() { -- if (this._smartcardManager?.loggedInWithToken()) -- return Const.SMARTCARD_SERVICE_NAME; -- else if (this._settings.get_boolean(PASSWORD_AUTHENTICATION_KEY)) -- return Const.PASSWORD_SERVICE_NAME; -- else if (this._smartcardManager) -- return Const.SMARTCARD_SERVICE_NAME; -- else if (this._fingerprintReaderFound) -- return Const.FINGERPRINT_SERVICE_NAME; -- return null; + this._updateAuthServices(); } +- _getDetectedDefaultService() { +- if (this._smartcardManager?.loggedInWithToken()) +- return Constants.SMARTCARD_SERVICE_NAME; +- else if (this._settings.get_boolean(PASSWORD_AUTHENTICATION_KEY)) +- return Constants.PASSWORD_SERVICE_NAME; +- else if (this._smartcardManager) +- return Constants.SMARTCARD_SERVICE_NAME; +- else if (this._fingerprintReaderFound) +- return Constants.FINGERPRINT_SERVICE_NAME; +- return null; +- } ++ _updateAuthServices() { ++ const enabledRoles = []; + - _updateDefaultService() { - const oldDefaultService = this._defaultService; - this._defaultService = this._getDetectedDefaultService(); -- ++ 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(FINGERPRINT_AUTHENTICATION_KEY)) ++ enabledRoles.push(Constants.FINGERPRINT_ROLE_NAME); + - if (!this._defaultService) { - log('no authentication service is enabled, using password authentication'); -- this._defaultService = Const.PASSWORD_SERVICE_NAME; +- this._defaultService = Constants.PASSWORD_SERVICE_NAME; - } -+ _updateAuthServices() { -+ const enabledRoles = []; - +- - if (oldDefaultService && - oldDefaultService !== this._defaultService && - this._activeServices.has(oldDefaultService)) - this._cancelAndReset(); - } -+ if (this._settings.get_boolean(PASSWORD_AUTHENTICATION_KEY)) -+ enabledRoles.push(Const.PASSWORD_ROLE_NAME); -+ if (this._settings.get_boolean(SMARTCARD_AUTHENTICATION_KEY)) -+ enabledRoles.push(Const.SMARTCARD_ROLE_NAME); -+ if (this._settings.get_boolean(FINGERPRINT_AUTHENTICATION_KEY)) -+ enabledRoles.push(Const.FINGERPRINT_ROLE_NAME); - +- - async _startService(serviceName) { - this._hold?.acquire(); - try { @@ -6379,11 +6764,10 @@ index ae464665e2..8c11c63c49 100644 - async _maybeStartFingerprintVerification() { - if (this._userName && - this._fingerprintReaderFound && -- !this.serviceIsForeground(Const.FINGERPRINT_SERVICE_NAME)) -- await this._startService(Const.FINGERPRINT_SERVICE_NAME); -+ this._createAuthServices(); - } - +- !this.serviceIsForeground(Constants.FINGERPRINT_SERVICE_NAME)) +- await this._startService(Constants.FINGERPRINT_SERVICE_NAME); +- } +- - _onChoiceListQuery(client, serviceName, promptMessage, list) { - if (!this.serviceIsForeground(serviceName)) - return; @@ -6402,7 +6786,7 @@ index ae464665e2..8c11c63c49 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 === FingerprintManager.FingerprintReaderType.SWIPE) { +- if (this._fingerprintReaderType === 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)'), @@ -6414,14 +6798,25 @@ index ae464665e2..8c11c63c49 100644 - MessageType.HINT); - } - } -- } -- ++ this._createAuthServices(); + } + - _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) { @@ -6450,24 +6845,17 @@ index ae464665e2..8c11c63c49 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) { @@ -6484,22 +6872,10 @@ index ae464665e2..8c11c63c49 100644 - } - - this.emit('ask-question', serviceName, secretQuestion, true); -+ _clearAuthServices() { -+ this._authServicesLegacy?.disconnectObject(this); -+ this._authServicesLegacy?.clear(); -+ this._authServicesLegacy = null; - } - -- _onReset() { -- // Clear previous attempts to authenticate -- this._failCounter = 0; -- this._activeServices.clear(); -- this._unavailableServices.clear(); -- this._updateDefaultService(); -- -- this.emit('reset'); + _connectAuthServices() { -+ [this._authServicesLegacy].forEach(authServices => { ++ [ ++ this._authServicesLegacy, ++ ].forEach(authServices => { + authServices?.connectObject( + 'ask-question', (_, ...args) => this.emit('ask-question', ...args), + 'queue-message', (_, ...args) => this._queueMessage(...args), @@ -6515,6 +6891,19 @@ index ae464665e2..8c11c63c49 100644 + }); } +- _onReset() { +- // Clear previous attempts to authenticate +- this._failCounter = 0; +- this._activeServices.clear(); +- this._unavailableServices.clear(); +- this._updateDefaultService(); +- +- this.emit('reset'); ++ _verificationFailed(serviceName, canRetry) { ++ this._filterServiceMessages(serviceName, MessageType.ERROR); ++ this.emit('verification-failed', serviceName, canRetry); + } + - _onVerificationComplete(_client, serviceName) { - const isCredentialManager = !!this._credentialManagers[serviceName]; - const isForeground = this.serviceIsForeground(serviceName); @@ -6524,9 +6913,8 @@ index ae464665e2..8c11c63c49 100644 - } - - 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() { @@ -6540,7 +6928,7 @@ index ae464665e2..8c11c63c49 100644 - this._connectSignals(); - this._startService(serviceName); - } -+ const selectedMechanism = this._authServicesLegacy?.selectedMechanism ?? ++ const selectedMechanism = this.selectedMechanism ?? + mechanisms.find(m => isSelectable(m)) ?? + {}; @@ -6551,7 +6939,7 @@ index ae464665e2..8c11c63c49 100644 } - async _verificationFailed(serviceName, shouldRetry) { -- if (serviceName === Const.FINGERPRINT_SERVICE_NAME) { +- if (serviceName === Constants.FINGERPRINT_SERVICE_NAME) { - if (this._fingerprintFailedId) - GLib.source_remove(this._fingerprintFailedId); - } @@ -6565,7 +6953,7 @@ index ae464665e2..8c11c63c49 100644 - const doneTrying = !shouldRetry || !this._canRetry(); - - this.emit('verification-failed', serviceName, !doneTrying); -+ async _waitPendingMessages(waiter) { ++ async _waitPendingMessages(task) { try { - if (doneTrying) { - this._disconnectSignals(); @@ -6576,14 +6964,14 @@ index ae464665e2..8c11c63c49 100644 - this._retry(serviceName); - } + await this._handlePendingMessages(); -+ waiter.resolve(); ++ task.return_boolean(true); } catch (e) { - logErrorUnlessCancelled(e); -+ waiter.reject(e); ++ task.return_error(e); } } -@@ -840,47 +457,4 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -849,47 +487,4 @@ export class ShellUserVerifier extends Signals.EventEmitter { }); }); } @@ -6632,7 +7020,7 @@ index ae464665e2..8c11c63c49 100644 - } } diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml -index 26b55b75bb..b6801acdb7 100644 +index 162e19d41..dce0c6acc 100644 --- a/js/js-resources.gresource.xml +++ b/js/js-resources.gresource.xml @@ -4,6 +4,8 @@ @@ -6642,25 +7030,10 @@ index 26b55b75bb..b6801acdb7 100644 + gdm/authServices.js + gdm/authServicesLegacy.js gdm/batch.js - gdm/const.js + gdm/constants.js gdm/credentialManager.js -diff --git a/js/misc/fingerprintManager.js b/js/misc/fingerprintManager.js -index 5bcb7bf6f0..609017a571 100644 ---- a/js/misc/fingerprintManager.js -+++ b/js/misc/fingerprintManager.js -@@ -67,10 +67,6 @@ class FingerprintManager extends GObject.Object { - return this.readerType !== FingerprintReaderType.NONE; - } - -- setDefaultTimeout(timeout) { -- this._fingerprintManagerProxy.set_default_timeout(timeout); -- } -- - async checkReaderType(cancellable) { - try { - // Wrappers don't support null cancellable, so let's ignore it in case diff --git a/po/POTFILES.in b/po/POTFILES.in -index ee0829c96e..eb58487b4c 100644 +index ee0829c96..eb58487b4 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -11,6 +11,7 @@ data/X-GNOME-Shell-Utilities.directory.desktop.in @@ -6672,13 +7045,13 @@ index ee0829c96e..eb58487b4c 100644 js/gdm/util.js js/misc/breakManager.js -- -2.53.0 +2.54.0 -From c2118d91c727bea67722313839d72db170993659 Mon Sep 17 00:00:00 2001 +From 8a6e99edeb08b1fa93ab82338723e13080e20b34 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Mon, 16 Feb 2026 12:21:54 +0100 -Subject: [PATCH 35/42] gdm: Add fingerprint ready state to delay showing icon +Subject: [PATCH 47/54] 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 @@ -6693,14 +7066,14 @@ fingerprint service starts but quickly stops with service-unavailable, so the timeout never completes and the icon never shows. --- js/gdm/authServices.js | 2 +- - js/gdm/authServicesLegacy.js | 55 ++++++++++++++++++++++++++++++++---- - 2 files changed, 51 insertions(+), 6 deletions(-) + js/gdm/authServicesLegacy.js | 54 ++++++++++++++++++++++++++++++++---- + 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/js/gdm/authServices.js b/js/gdm/authServices.js -index f702b15a04..7f0cda791a 100644 +index 8f05a5d8f..52fb5b27e 100644 --- a/js/gdm/authServices.js +++ b/js/gdm/authServices.js -@@ -87,7 +87,7 @@ export class AuthServices extends GObject.Object { +@@ -95,7 +95,7 @@ export class AuthServices extends GObject.Object { } get enabledMechanisms() { @@ -6710,10 +7083,10 @@ index f702b15a04..7f0cda791a 100644 get _roleToService() { diff --git a/js/gdm/authServicesLegacy.js b/js/gdm/authServicesLegacy.js -index e9c1676ab9..68a3c6990a 100644 +index a77c0fbc0..85c55610e 100644 --- a/js/gdm/authServicesLegacy.js +++ b/js/gdm/authServicesLegacy.js -@@ -9,6 +9,7 @@ import * as Vmware from './vmware.js'; +@@ -10,6 +10,7 @@ import * as Vmware from './vmware.js'; import {AuthServices} from './authServices.js'; const FINGERPRINT_ERROR_TIMEOUT_WAIT = 15; @@ -6721,16 +7094,16 @@ index e9c1676ab9..68a3c6990a 100644 const Mechanisms = [ { -@@ -55,6 +56,8 @@ export class AuthServicesLegacy extends AuthServices { +@@ -54,6 +55,8 @@ export class AuthServicesLegacy extends AuthServices { this._credentialManagers = {}; - this._addCredentialManager(OVirt.SERVICE_NAME, OVirt.getOVirtCredentialsManager()); - this._addCredentialManager(Vmware.SERVICE_NAME, Vmware.getVmwareCredentialsManager()); + this.addCredentialManager(OVirt.SERVICE_NAME, OVirt.getOVirtCredentialsManager()); + this.addCredentialManager(Vmware.SERVICE_NAME, Vmware.getVmwareCredentialsManager()); + + this._fingerprintReadyTimeoutId = 0; } _handleSelectChoice(serviceName, key) { -@@ -103,6 +106,39 @@ export class AuthServicesLegacy extends AuthServices { +@@ -100,6 +103,41 @@ export class AuthServicesLegacy extends AuthServices { _handleClear() { this._smartcardInProgress = false; @@ -6745,55 +7118,54 @@ index e9c1676ab9..68a3c6990a 100644 + } + + _handleOnConversationStarted(serviceName) { -+ if (serviceName === Const.FINGERPRINT_SERVICE_NAME && -+ this._fingerprintReadyTimeoutId === 0) { -+ this._fingerprintReadyTimeoutId = GLib.timeout_add( -+ GLib.PRIORITY_DEFAULT, -+ FINGERPRINT_READY_TIMEOUT_MS, -+ () => { -+ this._fingerprintReadyTimeoutId = 0; -+ this._setFingerprintReady(); -+ return GLib.SOURCE_REMOVE; -+ }); -+ } -+ } -+ -+ _setFingerprintReady() { -+ const mechanism = this._enabledMechanisms.find(m => -+ m.role === Const.FINGERPRINT_ROLE_NAME); -+ if (!mechanism || mechanism.ready) ++ if (serviceName !== Constants.FINGERPRINT_SERVICE_NAME || ++ this._fingerprintReadyTimeoutId !== 0) + return; + -+ mechanism.ready = true; ++ this._fingerprintReadyTimeoutId = GLib.timeout_add_once( ++ GLib.PRIORITY_DEFAULT, ++ FINGERPRINT_READY_TIMEOUT_MS, ++ () => { ++ this._fingerprintReadyTimeoutId = 0; ++ this._setFingerprintReady(true); ++ }); ++ } + -+ this.emit('mechanisms-changed'); ++ _setFingerprintReady(ready) { ++ const mechanism = this._enabledMechanisms.find(m => ++ m.role === Constants.FINGERPRINT_ROLE_NAME); ++ ++ if (!mechanism || mechanism.ready === ready) ++ return; ++ ++ mechanism.ready = ready; ++ ++ if (ready) ++ this.emit('mechanisms-changed'); } _handleUpdateEnabledMechanisms() { -@@ -115,6 +151,13 @@ export class AuthServicesLegacy extends AuthServices { +@@ -112,6 +150,10 @@ export class AuthServicesLegacy extends AuthServices { this._enabledMechanisms.push(...Mechanisms.filter(m => this._enabledRoles.includes(m.role) )); + + // Mark fingerprint as not ready until service confirms + // it's working for this user -+ const fingerprintMechanism = this._enabledMechanisms.find(m => -+ m.role === Const.FINGERPRINT_ROLE_NAME); -+ if (fingerprintMechanism) -+ fingerprintMechanism.ready = false; ++ this._setFingerprintReady(false); } } -@@ -232,11 +275,13 @@ export class AuthServicesLegacy extends AuthServices { +@@ -229,11 +271,13 @@ export class AuthServicesLegacy extends AuthServices { return; } -- if (serviceName === Const.FINGERPRINT_SERVICE_NAME && +- if (serviceName === Constants.FINGERPRINT_SERVICE_NAME && - this._unavailableServices.has(serviceName)) { - this._enabledMechanisms = this._enabledMechanisms - .filter(m => m.serviceName !== serviceName); - this.emit('mechanisms-changed'); -+ if (serviceName === Const.FINGERPRINT_SERVICE_NAME) { ++ if (serviceName === Constants.FINGERPRINT_SERVICE_NAME) { + this._clearFingerprintReadyTimeout(); + if (this._unavailableServices.has(serviceName)) { + this._enabledMechanisms = this._enabledMechanisms @@ -6804,13 +7176,13 @@ index e9c1676ab9..68a3c6990a 100644 if (this._unavailableServices.has(serviceName)) -- -2.53.0 +2.54.0 -From 0ec2d1f973cc28ab3f3b4bb5476b348627e987ba Mon Sep 17 00:00:00 2001 +From ec86c48f1c238cae30342b7f241eddb2e98208aa Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Tue, 6 Feb 2024 14:09:34 -0500 -Subject: [PATCH 36/42] gdm: Add authServicesSSSDSwitchable +Subject: [PATCH 48/54] 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' @@ -6844,36 +7216,48 @@ 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. --- - js/gdm/authServicesSSSDSwitchable.js | 167 +++++++++++++++++++++++++++ + js/gdm/authServices.js | 1 + + js/gdm/authServicesSSSDSwitchable.js | 166 +++++++++++++++++++++++++++ js/gdm/loginDialog.js | 5 +- - js/gdm/util.js | 46 ++++++-- + js/gdm/util.js | 42 ++++++- js/js-resources.gresource.xml | 1 + js/ui/unlockDialog.js | 1 + po/POTFILES.in | 1 + - 6 files changed, 213 insertions(+), 8 deletions(-) + 7 files changed, 210 insertions(+), 7 deletions(-) create mode 100644 js/gdm/authServicesSSSDSwitchable.js +diff --git a/js/gdm/authServices.js b/js/gdm/authServices.js +index 52fb5b27e..4394848ba 100644 +--- a/js/gdm/authServices.js ++++ b/js/gdm/authServices.js +@@ -18,6 +18,7 @@ Gio._promisify(Gdm.UserVerifierProxy.prototype, 'call_begin_verification_for_use + Gio._promisify(Gdm.UserVerifierProxy.prototype, 'call_begin_verification'); + 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 { + static [GObject.signals] = { diff --git a/js/gdm/authServicesSSSDSwitchable.js b/js/gdm/authServicesSSSDSwitchable.js new file mode 100644 -index 0000000000..b6e2e06697 +index 000000000..c67933b6f --- /dev/null +++ b/js/gdm/authServicesSSSDSwitchable.js -@@ -0,0 +1,167 @@ +@@ -0,0 +1,166 @@ +import GObject from 'gi://GObject'; + -+import * as Const from './const.js'; ++import * as Constants from './constants.js'; ++import {logErrorUnlessCancelled} from '../misc/errorUtils.js'; +import * as Util from './util.js'; +import {AuthServices} from './authServices.js'; + +export class AuthServicesSSSDSwitchable extends AuthServices { -+ static [GObject.GTypeName] = 'AuthServicesSSSDSwitchable'; -+ + static SupportedRoles = [ -+ Const.PASSWORD_ROLE_NAME, ++ Constants.PASSWORD_ROLE_NAME, + ]; + + static RoleToService = { -+ [Const.PASSWORD_ROLE_NAME]: Const.SWITCHABLE_AUTH_SERVICE_NAME, ++ [Constants.PASSWORD_ROLE_NAME]: Constants.SWITCHABLE_AUTH_SERVICE_NAME, + }; + + static { @@ -6890,7 +7274,7 @@ index 0000000000..b6e2e06697 + + let response; + switch (this._selectedMechanism.role) { -+ case Const.PASSWORD_ROLE_NAME: ++ case Constants.PASSWORD_ROLE_NAME: + response = this._formatResponse(answer); + this._sendResponse(response); + break; @@ -6899,7 +7283,7 @@ index 0000000000..b6e2e06697 + + _handleSelectMechanism() { + switch (this._selectedMechanism?.role) { -+ case Const.PASSWORD_ROLE_NAME: ++ case Constants.PASSWORD_ROLE_NAME: + this._startPasswordLogin(); + break; + } @@ -6944,7 +7328,7 @@ index 0000000000..b6e2e06697 + _handleUpdateEnabledMechanisms() { + this._enabledMechanisms.push(...Object.keys(this._mechanisms) + .map(id => ({ -+ serviceName: Const.SWITCHABLE_AUTH_SERVICE_NAME, ++ serviceName: Constants.SWITCHABLE_AUTH_SERVICE_NAME, + id, + ...this._mechanisms[id], + })) @@ -6988,7 +7372,7 @@ index 0000000000..b6e2e06697 + } + + _handleCanStartService(serviceName) { -+ return serviceName === Const.SWITCHABLE_AUTH_SERVICE_NAME && ++ return serviceName === Constants.SWITCHABLE_AUTH_SERVICE_NAME && + !this._enabledMechanisms; + } + @@ -6997,7 +7381,7 @@ index 0000000000..b6e2e06697 + + let response; + switch (role) { -+ case Const.PASSWORD_ROLE_NAME: { ++ case Constants.PASSWORD_ROLE_NAME: { + response = {password: answer}; + break; + } @@ -7017,7 +7401,7 @@ index 0000000000..b6e2e06697 + const {serviceName} = this._selectedMechanism; + + this._userVerifierCustomJSON.call_reply( -+ serviceName, JSON.stringify(response), this._cancellable, null); ++ serviceName, JSON.stringify(response), this._cancellable).catch(logErrorUnlessCancelled); + } + + _startPasswordLogin() { @@ -7027,10 +7411,10 @@ index 0000000000..b6e2e06697 + } +} diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js -index ddb7e603b2..4c08f0957e 100644 +index 8656bdf29..69573adfe 100644 --- a/js/gdm/loginDialog.js +++ b/js/gdm/loginDialog.js -@@ -452,7 +452,10 @@ export const LoginDialog = GObject.registerClass({ +@@ -453,7 +453,10 @@ export const LoginDialog = GObject.registerClass({ this._gdmClient = new Gdm.Client(); try { @@ -7043,18 +7427,18 @@ index ddb7e603b2..4c08f0957e 100644 } diff --git a/js/gdm/util.js b/js/gdm/util.js -index 8c11c63c49..ab4eccec42 100644 +index ddfecd0cf..5952947e7 100644 --- a/js/gdm/util.js +++ b/js/gdm/util.js -@@ -8,6 +8,7 @@ import * as Const from './const.js'; - import * as Main from '../ui/main.js'; +@@ -9,6 +9,7 @@ import * as Main from '../ui/main.js'; + import {logErrorUnlessCancelled} from '../misc/errorUtils.js'; import * as Params from '../misc/params.js'; import {AuthServicesLegacy} from './authServicesLegacy.js'; +import {AuthServicesSSSDSwitchable} from './authServicesSSSDSwitchable.js'; const CLONE_FADE_ANIMATION_TIME = 250; -@@ -15,6 +16,7 @@ export const LOGIN_SCREEN_SCHEMA = 'org.gnome.login-screen'; +@@ -16,6 +17,7 @@ export const LOGIN_SCREEN_SCHEMA = 'org.gnome.login-screen'; export const PASSWORD_AUTHENTICATION_KEY = 'enable-password-authentication'; export const FINGERPRINT_AUTHENTICATION_KEY = 'enable-fingerprint-authentication'; export const SMARTCARD_AUTHENTICATION_KEY = 'enable-smartcard-authentication'; @@ -7062,7 +7446,7 @@ index 8c11c63c49..ab4eccec42 100644 export const BANNER_MESSAGE_KEY = 'banner-message-enable'; export const BANNER_MESSAGE_SOURCE_KEY = 'banner-message-source'; export const BANNER_MESSAGE_TEXT_KEY = 'banner-message-text'; -@@ -150,6 +152,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -162,6 +164,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { try { const proxies = await this._getUserVerifierProxies(userName, this._cancellable); @@ -7070,7 +7454,7 @@ index 8c11c63c49..ab4eccec42 100644 await this._authServicesLegacy?.beginVerification(userName, proxies); this._userVerifier = proxies.userVerifier; } catch (e) { -@@ -161,14 +164,19 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -173,14 +176,19 @@ export class ShellUserVerifier extends Signals.EventEmitter { } selectMechanism(mechanism) { @@ -7092,7 +7476,7 @@ index 8c11c63c49..ab4eccec42 100644 this._authServicesLegacy?.reset(); this._userVerifier?.call_cancel_sync(null); -@@ -177,6 +185,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -189,6 +197,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { } cancel() { @@ -7100,7 +7484,7 @@ index 8c11c63c49..ab4eccec42 100644 this._authServicesLegacy?.cancel(); this._userVerifier?.call_cancel_sync(null); -@@ -185,6 +194,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -197,6 +206,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { } clear() { @@ -7108,15 +7492,17 @@ index 8c11c63c49..ab4eccec42 100644 this._authServicesLegacy?.clear(); this._clearMessageQueue(); -@@ -196,6 +206,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -208,6 +218,9 @@ export class ShellUserVerifier extends Signals.EventEmitter { } destroy() { -+ this._authServicesSSSDSwitchable?.disconnectObject(this); - this._authServicesLegacy?.disconnectObject(this); ++ this._authServicesSSSDSwitchable?.destroy(); ++ this._authServicesSSSDSwitchable = null; ++ + this._authServicesLegacy?.destroy(); + this._authServicesLegacy = null; - this.cancel(); -@@ -205,10 +216,12 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -218,6 +231,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { } selectChoice(serviceName, key) { @@ -7124,14 +7510,17 @@ index 8c11c63c49..ab4eccec42 100644 this._authServicesLegacy?.selectChoice(serviceName, key); } - answerQuery(serviceName, answer) { +@@ -226,6 +240,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { + // ensure no messages get lost + await this._handlePendingMessages().catch(logErrorUnlessCancelled); + + this._authServicesSSSDSwitchable?.answerQuery(serviceName, answer); this._authServicesLegacy?.answerQuery(serviceName, answer); } -@@ -372,10 +385,15 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -397,10 +412,15 @@ export class ShellUserVerifier extends Signals.EventEmitter { if (this._settings.get_boolean(FINGERPRINT_AUTHENTICATION_KEY)) - enabledRoles.push(Const.FINGERPRINT_ROLE_NAME); + enabledRoles.push(Constants.FINGERPRINT_ROLE_NAME); - if (JSON.stringify(enabledRoles) === JSON.stringify(this._enabledRoles)) + const switchableAuthentication = @@ -7146,7 +7535,7 @@ index 8c11c63c49..ab4eccec42 100644 this._createAuthServices(); } -@@ -389,20 +407,30 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -414,19 +434,25 @@ export class ShellUserVerifier extends Signals.EventEmitter { allowedFailures: this.allowedFailures, reauthOnly: this._reauthOnly, }; @@ -7161,25 +7550,26 @@ index 8c11c63c49..ab4eccec42 100644 } _clearAuthServices() { -+ this._authServicesSSSDSwitchable?.disconnectObject(this); -+ this._authServicesSSSDSwitchable?.clear(); ++ this._authServicesSSSDSwitchable?.destroy(); + this._authServicesSSSDSwitchable = null; -+ - this._authServicesLegacy?.disconnectObject(this); - this._authServicesLegacy?.clear(); + this._authServicesLegacy?.destroy(); this._authServicesLegacy = null; } _connectAuthServices() { -- [this._authServicesLegacy].forEach(authServices => { -+ [ + [ + this._authServicesSSSDSwitchable, -+ this._authServicesLegacy, -+ ].forEach(authServices => { + this._authServicesLegacy, + ].forEach(authServices => { authServices?.connectObject( - 'ask-question', (_, ...args) => this.emit('ask-question', ...args), - 'queue-message', (_, ...args) => this._queueMessage(...args), -@@ -424,9 +452,13 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -450,11 +476,15 @@ export class ShellUserVerifier extends Signals.EventEmitter { + } + + get selectedMechanism() { +- return this._authServicesLegacy?.selectedMechanism ?? null; ++ return this._authServicesSSSDSwitchable?.selectedMechanism ?? ++ this._authServicesLegacy?.selectedMechanism ?? ++ null; } _onMechanismsChanged() { @@ -7188,15 +7578,10 @@ index 8c11c63c49..ab4eccec42 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 b6801acdb7..a7bcbd8898 100644 +index dce0c6acc..93377b92c 100644 --- a/js/js-resources.gresource.xml +++ b/js/js-resources.gresource.xml @@ -6,6 +6,7 @@ @@ -7205,13 +7590,13 @@ index b6801acdb7..a7bcbd8898 100644 gdm/authServicesLegacy.js + gdm/authServicesSSSDSwitchable.js gdm/batch.js - gdm/const.js + gdm/constants.js gdm/credentialManager.js diff --git a/js/ui/unlockDialog.js b/js/ui/unlockDialog.js -index 80c3dd813a..a16f68fee3 100644 +index af584470a..50865198a 100644 --- a/js/ui/unlockDialog.js +++ b/js/ui/unlockDialog.js -@@ -570,6 +570,7 @@ export const UnlockDialog = GObject.registerClass({ +@@ -567,6 +567,7 @@ export const UnlockDialog = GObject.registerClass({ try { this._gdmClient.set_enabled_extensions([ Gdm.UserVerifierChoiceList.interface_info().name, @@ -7220,7 +7605,7 @@ index 80c3dd813a..a16f68fee3 100644 } catch { } diff --git a/po/POTFILES.in b/po/POTFILES.in -index eb58487b4c..d1e4d64872 100644 +index eb58487b4..d1e4d6487 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -12,6 +12,7 @@ js/dbusServices/extensions/extensionPrefsDialog.js @@ -7232,13 +7617,13 @@ index eb58487b4c..d1e4d64872 100644 js/gdm/util.js js/misc/breakManager.js -- -2.53.0 +2.54.0 -From baa6d1f833d409208661e69e5474158e94e8f089 Mon Sep 17 00:00:00 2001 +From 9fe8dcb095a27f0acc4090618eee101c9fcc0812 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Wed, 12 Nov 2025 17:25:33 +0100 -Subject: [PATCH 37/42] authServicesSSSDSwitchable: Allow resetting expired +Subject: [PATCH 49/54] authServicesSSSDSwitchable: Allow resetting expired password JSON protocol can't inform when a password is expired, so it's needed to @@ -7249,35 +7634,29 @@ 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. --- - js/gdm/authServicesSSSDSwitchable.js | 27 ++++++++++++++++++++++++++- - 1 file changed, 26 insertions(+), 1 deletion(-) + js/gdm/authServicesSSSDSwitchable.js | 24 ++++++++++++++++++++++++ + 1 file changed, 24 insertions(+) diff --git a/js/gdm/authServicesSSSDSwitchable.js b/js/gdm/authServicesSSSDSwitchable.js -index b6e2e06697..5f89478ff9 100644 +index c67933b6f..91bdde5c7 100644 --- a/js/gdm/authServicesSSSDSwitchable.js +++ b/js/gdm/authServicesSSSDSwitchable.js -@@ -23,10 +23,19 @@ export class AuthServicesSSSDSwitchable extends AuthServices { - super(params); - } - -- _handleAnswerQuery(serviceName, answer) { -+ async _handleAnswerQuery(serviceName, answer) { +@@ -26,6 +26,14 @@ export class AuthServicesSSSDSwitchable extends AuthServices { if (serviceName !== this._selectedMechanism?.serviceName) return; -+ if (this._selectedMechanism.role === Const.PASSWORD_ROLE_NAME && ++ if (this._selectedMechanism.role === Constants.PASSWORD_ROLE_NAME && + this._resettingPassword) { -+ await this._userVerifier.call_answer_query(serviceName, ++ this._userVerifier.call_answer_query(serviceName, + answer, -+ this._cancellable, -+ null); ++ this._cancellable).catch(logErrorUnlessCancelled); + return; + } + let response; switch (this._selectedMechanism.role) { - case Const.PASSWORD_ROLE_NAME: -@@ -58,6 +67,8 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + case Constants.PASSWORD_ROLE_NAME: +@@ -57,6 +65,8 @@ export class AuthServicesSSSDSwitchable extends AuthServices { this._priorityList = null; this._enabledMechanisms = null; this._selectedMechanism = null; @@ -7286,27 +7665,27 @@ index b6e2e06697..5f89478ff9 100644 } _handleOnCustomJSONRequest(_serviceName, _protocol, _version, json) { -@@ -102,6 +113,13 @@ export class AuthServicesSSSDSwitchable extends AuthServices { +@@ -101,6 +111,13 @@ export class AuthServicesSSSDSwitchable extends AuthServices { } _handleOnInfo(serviceName, info) { + // 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 === Const.PASSWORD_ROLE_NAME && ++ this._selectedMechanism.role === Constants.PASSWORD_ROLE_NAME && + 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); } -@@ -115,6 +133,13 @@ export class AuthServicesSSSDSwitchable extends AuthServices { +@@ -114,6 +131,13 @@ export class AuthServicesSSSDSwitchable extends AuthServices { } } + _handleOnSecretInfoQuery(serviceName, secretQuestion) { + if (serviceName === this._selectedMechanism?.serviceName && -+ this._selectedMechanism.role === Const.PASSWORD_ROLE_NAME && ++ this._selectedMechanism.role === Constants.PASSWORD_ROLE_NAME && + this._resettingPassword) + this.emit('ask-question', serviceName, secretQuestion, true); + } @@ -7315,13 +7694,16 @@ index b6e2e06697..5f89478ff9 100644 if (serviceName !== this._selectedMechanism?.serviceName) return; -- -2.53.0 +2.54.0 -From 901bb24c5228ab67f417abfe32c1ffde066be986 Mon Sep 17 00:00:00 2001 +From 2aebcf41fa56e742b7d0f70508df630b003da92e Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Thu, 21 Aug 2025 22:25:02 +0200 -Subject: [PATCH 38/42] gdm: Allow starting authServicesLegacy as fallback +Subject: [PATCH 50/54] gdm: Allow starting authServicesLegacy as fallback +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit There can be cases where authServicesSSSDSwitchable doesn't support some authentication methods. @@ -7332,23 +7714,33 @@ authServicesSSSDSwitchable leads when the fallback will happen emitting 'mechanisms-changed'. Then unsupportedRoles, from authServicesSSSDSwitchable will be used to update the enabledRoles in authServicesLegacy. -While authServicesSSSDSwitchable hasn't started its service and didn't receive -any mechanisms from JSON protocol, all roles are considered unsupported. -If it receives any message from GDM userVerifier before receiving -'auth-selection' JSON message, that will mean that pam_unix is being used and -it should fallback to authServicesLegacy (all roles are unsupported). +The supportedRoles and unsupportedRoles properties work together: +- supportedRoles: static per class, defines roles the class can handle +- unsupportedRoles: dynamic, roles from enabledRoles that should be delegated + to another auth service as fallback + +For authServicesSSSDSwitchable: +- WAITING/FOUND state: unsupportedRoles returns enabledRoles not in supportedRoles + (e.g., fingerprint). If a role like password isn't in the JSON mechanisms, + that means it's not enabled for this user — authServicesLegacy shouldn't + handle it. +- NOT_FOUND state: unsupportedRoles returns all enabledRoles, since pam_sss + isn't handling this user and authServicesLegacy must take over completely. + +This way authServicesSSSDSwitchable controls which roles authServicesLegacy +handles, enabling coordination between both services. --- - js/gdm/authServices.js | 22 ++++++++++ + js/gdm/authServices.js | 22 +++++++++ js/gdm/authServicesLegacy.js | 5 +++ - js/gdm/authServicesSSSDSwitchable.js | 62 +++++++++++++++++++++++++++- - js/gdm/util.js | 28 +++++++++++-- - 4 files changed, 111 insertions(+), 6 deletions(-) + js/gdm/authServicesSSSDSwitchable.js | 67 +++++++++++++++++++++++++++- + js/gdm/util.js | 28 ++++++++++-- + 4 files changed, 116 insertions(+), 6 deletions(-) diff --git a/js/gdm/authServices.js b/js/gdm/authServices.js -index 7f0cda791a..e2dbba8aff 100644 +index 4394848ba..58905d15a 100644 --- a/js/gdm/authServices.js +++ b/js/gdm/authServices.js -@@ -98,6 +98,10 @@ export class AuthServices extends GObject.Object { +@@ -107,6 +107,10 @@ export class AuthServices extends GObject.Object { return this.constructor.SupportedRoles; } @@ -7359,7 +7751,7 @@ index 7f0cda791a..e2dbba8aff 100644 selectChoice(serviceName, key) { this._handleSelectChoice(serviceName, key); } -@@ -173,6 +177,18 @@ export class AuthServices extends GObject.Object { +@@ -183,6 +187,18 @@ export class AuthServices extends GObject.Object { this._handleClear(); } @@ -7378,7 +7770,7 @@ index 7f0cda791a..e2dbba8aff 100644 _clearUserVerifier() { this._disconnectUserVerifierSignals(); this._userVerifier = null; -@@ -416,6 +432,10 @@ export class AuthServices extends GObject.Object { +@@ -414,6 +430,10 @@ export class AuthServices extends GObject.Object { } } @@ -7388,8 +7780,8 @@ index 7f0cda791a..e2dbba8aff 100644 + _handleSelectChoice() {} - async _handleAnswerQuery() {} -@@ -434,6 +454,8 @@ export class AuthServices extends GObject.Object { + _handleAnswerQuery() {} +@@ -432,6 +452,8 @@ export class AuthServices extends GObject.Object { _handleClear() {} @@ -7399,11 +7791,11 @@ index 7f0cda791a..e2dbba8aff 100644 throw new GObject.NotImplementedError( `_handleUpdateEnabledMechanisms in ${this.constructor.name}`); diff --git a/js/gdm/authServicesLegacy.js b/js/gdm/authServicesLegacy.js -index 68a3c6990a..0813622a01 100644 +index 85c55610e..132c5307c 100644 --- a/js/gdm/authServicesLegacy.js +++ b/js/gdm/authServicesLegacy.js -@@ -141,6 +141,11 @@ export class AuthServicesLegacy extends AuthServices { - this.emit('mechanisms-changed'); +@@ -140,6 +140,11 @@ export class AuthServicesLegacy extends AuthServices { + this.emit('mechanisms-changed'); } + _handleUpdateEnabledRoles() { @@ -7415,10 +7807,10 @@ index 68a3c6990a..0813622a01 100644 if (!this._fingerprintManager?.readerFound) { this._enabledMechanisms.push(...Mechanisms.filter(m => diff --git a/js/gdm/authServicesSSSDSwitchable.js b/js/gdm/authServicesSSSDSwitchable.js -index 5f89478ff9..9649dd1382 100644 +index 91bdde5c7..59f2765f6 100644 --- a/js/gdm/authServicesSSSDSwitchable.js +++ b/js/gdm/authServicesSSSDSwitchable.js -@@ -4,6 +4,12 @@ import * as Const from './const.js'; +@@ -5,6 +5,12 @@ import {logErrorUnlessCancelled} from '../misc/errorUtils.js'; import * as Util from './util.js'; import {AuthServices} from './authServices.js'; @@ -7429,9 +7821,9 @@ index 5f89478ff9..9649dd1382 100644 +}; + export class AuthServicesSSSDSwitchable extends AuthServices { - static [GObject.GTypeName] = 'AuthServicesSSSDSwitchable'; - -@@ -21,6 +27,8 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + static SupportedRoles = [ + Constants.PASSWORD_ROLE_NAME, +@@ -20,6 +26,8 @@ export class AuthServicesSSSDSwitchable extends AuthServices { constructor(params) { super(params); @@ -7439,14 +7831,16 @@ index 5f89478ff9..9649dd1382 100644 + this._mechanismsStatus = MechanismsStatus.WAITING; } - async _handleAnswerQuery(serviceName, answer) { -@@ -53,13 +61,30 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + _handleAnswerQuery(serviceName, answer) { +@@ -51,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: @@ -7472,7 +7866,17 @@ index 5f89478ff9..9649dd1382 100644 } _handleClear() { -@@ -89,6 +114,10 @@ export class AuthServicesSSSDSwitchable extends AuthServices { +@@ -72,6 +99,9 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + _handleOnCustomJSONRequest(_serviceName, _protocol, _version, json) { + let requestObject; + ++ if (this._mechanismsStatus !== MechanismsStatus.WAITING) ++ log('Received unexpected JSON message, something might be wrong'); ++ + try { + requestObject = JSON.parse(json); + } catch (e) { +@@ -87,6 +117,10 @@ export class AuthServicesSSSDSwitchable extends AuthServices { if (this._mechanisms) this._updateEnabledMechanisms(); } @@ -7483,7 +7887,7 @@ index 5f89478ff9..9649dd1382 100644 } _handleUpdateEnabledMechanisms() { -@@ -113,6 +142,9 @@ export class AuthServicesSSSDSwitchable extends AuthServices { +@@ -111,6 +145,9 @@ export class AuthServicesSSSDSwitchable extends AuthServices { } _handleOnInfo(serviceName, info) { @@ -7493,7 +7897,7 @@ index 5f89478ff9..9649dd1382 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 && -@@ -125,6 +157,9 @@ export class AuthServicesSSSDSwitchable extends AuthServices { +@@ -123,6 +160,9 @@ export class AuthServicesSSSDSwitchable extends AuthServices { } _handleOnProblem(serviceName, problem) { @@ -7503,7 +7907,7 @@ index 5f89478ff9..9649dd1382 100644 if (serviceName === this._selectedMechanism?.serviceName) { this.emit('queue-priority-message', serviceName, -@@ -133,7 +168,16 @@ export class AuthServicesSSSDSwitchable extends AuthServices { +@@ -131,7 +171,16 @@ export class AuthServicesSSSDSwitchable extends AuthServices { } } @@ -7518,19 +7922,19 @@ index 5f89478ff9..9649dd1382 100644 + return; + if (serviceName === this._selectedMechanism?.serviceName && - this._selectedMechanism.role === Const.PASSWORD_ROLE_NAME && + this._selectedMechanism.role === Constants.PASSWORD_ROLE_NAME && this._resettingPassword) -@@ -153,7 +197,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { +@@ -151,7 +200,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { _handleCanStartService(serviceName) { - return serviceName === Const.SWITCHABLE_AUTH_SERVICE_NAME && + return serviceName === Constants.SWITCHABLE_AUTH_SERVICE_NAME && - !this._enabledMechanisms; + this._mechanismsStatus === MechanismsStatus.WAITING; } _formatResponse(answer) { -@@ -184,6 +228,20 @@ export class AuthServicesSSSDSwitchable extends AuthServices { - serviceName, JSON.stringify(response), this._cancellable, null); +@@ -182,6 +231,20 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + serviceName, JSON.stringify(response), this._cancellable).catch(logErrorUnlessCancelled); } + _eventExpected() { @@ -7551,10 +7955,10 @@ index 5f89478ff9..9649dd1382 100644 const {serviceName, prompt} = this._selectedMechanism; diff --git a/js/gdm/util.js b/js/gdm/util.js -index ab4eccec42..36a55822cc 100644 +index 5952947e7..388afbc30 100644 --- a/js/gdm/util.js +++ b/js/gdm/util.js -@@ -197,6 +197,11 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -209,6 +209,11 @@ export class ShellUserVerifier extends Signals.EventEmitter { this._authServicesSSSDSwitchable?.clear(); this._authServicesLegacy?.clear(); @@ -7566,7 +7970,7 @@ index ab4eccec42..36a55822cc 100644 this._clearMessageQueue(); this._cancellable?.cancel(); -@@ -408,10 +413,14 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -435,10 +440,14 @@ export class ShellUserVerifier extends Signals.EventEmitter { reauthOnly: this._reauthOnly, }; if (this._switchableAuthenticationEnabled && @@ -7583,7 +7987,7 @@ index ab4eccec42..36a55822cc 100644 this._connectAuthServices(); } -@@ -452,9 +461,12 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -482,9 +491,12 @@ export class ShellUserVerifier extends Signals.EventEmitter { } _onMechanismsChanged() { @@ -7596,9 +8000,9 @@ index ab4eccec42..36a55822cc 100644 - const mechanisms = [...mechanismsSwitchable, ...mechanismsLegacy]; + const mechanisms = [...mechanismsSSSDSwitchable, ...mechanismsLegacy]; - const selectedMechanism = - this._authServicesSSSDSwitchable?.selectedMechanism ?? -@@ -465,6 +477,14 @@ export class ShellUserVerifier extends Signals.EventEmitter { + const selectedMechanism = this.selectedMechanism ?? + mechanisms.find(m => isSelectable(m)) ?? +@@ -493,6 +505,14 @@ export class ShellUserVerifier extends Signals.EventEmitter { this.emit('mechanisms-changed', mechanisms, selectedMechanism); } @@ -7610,17 +8014,17 @@ index ab4eccec42..36a55822cc 100644 + this._authServicesSSSDSwitchable.unsupportedRoles); + } + - async _waitPendingMessages(waiter) { + async _waitPendingMessages(task) { try { await this._handlePendingMessages(); -- -2.53.0 +2.54.0 -From e3e3917d895f852c286bde7030365f25c6c024a2 Mon Sep 17 00:00:00 2001 +From 7cff7141e54bae1982d0785c244695020b86b1d2 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 39/42] ui/qrCode: Add a QR Code widget +Subject: [PATCH 51/54] 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 @@ -7639,13 +8043,13 @@ theme data/theme/meson.build | 1 + js/js-resources.gresource.xml | 1 + js/misc/dependencies.js | 1 + - js/ui/qrCode.js | 119 ++++++++++++++++++ - 6 files changed, 137 insertions(+) + js/ui/qrCode.js | 118 ++++++++++++++++++ + 6 files changed, 136 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 9eac62d958..1e3fefa9d7 100644 +index 9eac62d95..1e3fefa9d 100644 --- a/data/theme/gnome-shell-sass/_widgets.scss +++ b/data/theme/gnome-shell-sass/_widgets.scss @@ -45,5 +45,6 @@ @@ -7657,7 +8061,7 @@ index 9eac62d958..1e3fefa9d7 100644 @import 'widgets/login-lock'; 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 @@ @@ -7676,7 +8080,7 @@ index 0000000000..da04425bd6 + } +} diff --git a/data/theme/meson.build b/data/theme/meson.build -index 8d01ae826d..d6f2d8e802 100644 +index 8d01ae826..d6f2d8e80 100644 --- a/data/theme/meson.build +++ b/data/theme/meson.build @@ -28,6 +28,7 @@ theme_sources = files([ @@ -7688,7 +8092,7 @@ index 8d01ae826d..d6f2d8e802 100644 'gnome-shell-sass/widgets/_screenshot.scss', 'gnome-shell-sass/widgets/_scrollbars.scss', diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml -index a7bcbd8898..59a428df9a 100644 +index 93377b92c..1da999963 100644 --- a/js/js-resources.gresource.xml +++ b/js/js-resources.gresource.xml @@ -108,6 +108,7 @@ @@ -7700,7 +8104,7 @@ index a7bcbd8898..59a428df9a 100644 ui/ripples.js ui/runDialog.js diff --git a/js/misc/dependencies.js b/js/misc/dependencies.js -index c3c51aa2de..fbb83d1164 100644 +index c3c51aa2d..fbb83d116 100644 --- a/js/misc/dependencies.js +++ b/js/misc/dependencies.js @@ -16,6 +16,7 @@ import 'gi://GioUnix?version=2.0'; @@ -7713,10 +8117,10 @@ index c3c51aa2de..fbb83d1164 100644 import 'gi://GUdev?version=1.0'; diff --git a/js/ui/qrCode.js b/js/ui/qrCode.js new file mode 100644 -index 0000000000..5f2bb85557 +index 000000000..caf0999f3 --- /dev/null +++ b/js/ui/qrCode.js -@@ -0,0 +1,119 @@ +@@ -0,0 +1,118 @@ +import Clutter from 'gi://Clutter'; +import Cogl from 'gi://Cogl'; +import GObject from 'gi://GObject'; @@ -7731,7 +8135,6 @@ index 0000000000..5f2bb85557 +const QR_CODE_TRANSPARENT_COLOR = new GnomeQR.Color({alpha: 0}); + +export class QrCode extends St.Bin { -+ static [GObject.GTypeName] = 'QrCode'; + static [GObject.properties] = { + 'url': GObject.ParamSpec.string( + 'url', null, null, @@ -7837,13 +8240,13 @@ index 0000000000..5f2bb85557 + } +}; -- -2.53.0 +2.54.0 -From d056723159604b6c251e273972599250bedc3897 Mon Sep 17 00:00:00 2001 +From 0335ac55a34c7ba4899449c7489931ac8aca3d7e Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Tue, 6 Feb 2024 14:18:24 -0500 -Subject: [PATCH 40/42] gdm: Add support for Web Login in +Subject: [PATCH 52/54] gdm: Add support for Web Login in authServicesSSSDSwitchable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 @@ -7859,37 +8262,37 @@ Use the bindings of the new GnomeQR library to generate the QR code. Co-authored-by: Marco Trevisan (Treviño) --- - .../gnome-shell-sass/widgets/_login-lock.scss | 100 ++++++ - js/gdm/authPrompt.js | 104 +++++- + .../gnome-shell-sass/widgets/_login-lock.scss | 86 +++++ + js/gdm/authPrompt.js | 106 +++++- js/gdm/authServices.js | 6 + - js/gdm/authServicesSSSDSwitchable.js | 82 +++++ - js/gdm/const.js | 1 + - js/gdm/loginDialog.js | 5 +- + js/gdm/authServicesSSSDSwitchable.js | 80 +++++ + js/gdm/constants.js | 1 + + js/gdm/loginDialog.js | 2 +- js/gdm/util.js | 5 + - js/gdm/webLogin.js | 310 ++++++++++++++++++ + js/gdm/webLogin.js | 308 ++++++++++++++++++ js/js-resources.gresource.xml | 1 + - js/ui/unlockDialog.js | 3 +- js/ui/userWidget.js | 8 + po/POTFILES.in | 1 + - 12 files changed, 614 insertions(+), 12 deletions(-) + 11 files changed, 593 insertions(+), 11 deletions(-) create mode 100644 js/gdm/webLogin.js diff --git a/data/theme/gnome-shell-sass/widgets/_login-lock.scss b/data/theme/gnome-shell-sass/widgets/_login-lock.scss -index c18a6bb8a8..ec76f01252 100644 +index 078b5c5c2..1ec15e417 100644 --- a/data/theme/gnome-shell-sass/widgets/_login-lock.scss +++ b/data/theme/gnome-shell-sass/widgets/_login-lock.scss -@@ -15,6 +15,10 @@ $_gdm_dialog_width: 25em; +@@ -15,6 +15,11 @@ $_gdm_dialog_width: 25em; .login-dialog-prompt-layout { - width: $_gdm_dialog_width; - spacing: $base_padding * 1.5; + width: $_gdm_dialog_width * 1.2; + margin-top: 80px; + + &.web-login-active { + width: $_gdm_dialog_width * 1.5; ++ margin-top: 0; + } } - .login-dialog-prompt-entry-area { -@@ -324,8 +328,88 @@ $_gdm_dialog_width: 25em; + .login-dialog-input-well { +@@ -349,8 +354,89 @@ $_gdm_dialog_width: 25em; } } @@ -7955,6 +8358,7 @@ index c18a6bb8a8..ec76f01252 100644 + + .web-login-intro-button { + padding: 0; ++ margin: 0.2em $base_margin * 5; + } + + .web-login-prompt-button { @@ -7978,28 +8382,8 @@ index c18a6bb8a8..ec76f01252 100644 .unlock-dialog { background-color: transparent; -@@ -483,3 +567,19 @@ $_gdm_dialog_width: 25em; - } - } - } -+ -+// QR Code -+.qr-code { -+ border-radius: $base_border_radius * .5; -+ border-width: 1em; -+ @if ($variant == 'light') { -+ $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: $_gdm_fg; -+ border-color: $_gdm_fg; -+ color: $_gdm_bg; -+ } -+} diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index d8c7c6409d..cdad827764 100644 +index db4c1647a..e511b3881 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js @@ -14,6 +14,7 @@ import * as GdmUtil from './util.js'; @@ -8018,13 +8402,14 @@ index d8c7c6409d..cdad827764 100644 'verification-failed', this._onVerificationFailed.bind(this), 'verification-complete', this._onVerificationComplete.bind(this), 'reset', this._onReset.bind(this), -@@ -305,6 +307,28 @@ export const AuthPrompt = GObject.registerClass({ - this._defaultButtonWell.add_child(this._spinner); +@@ -291,6 +293,29 @@ export const AuthPrompt = GObject.registerClass({ this.setActorInDefaultButtonWell(this._nextButton); -+ + + this._webLoginIntro = new WebLogin.WebLoginIntro(); + this._webLoginIntro.set({ ++ x_align: Clutter.ActorAlign.CENTER, ++ x_expand: true, + y_expand: true, + }); + this._webLoginIntro.connect('clicked', () => { @@ -8043,15 +8428,22 @@ index d8c7c6409d..cdad827764 100644 + } + }); + this._webLoginDialog.connect('loading', () => this.emit('loading', this._webLoginDialog.isLoading)); -+ this.add_child(this._webLoginDialog); ++ this._inputWell.add_child(this._webLoginDialog); + + // center elements inside _mainBox between the cancel + // button on the left and this spacer on the right +@@ -445,10 +470,58 @@ export const AuthPrompt = GObject.registerClass({ + if (message && + type < GdmUtil.MessageType.ERROR && + !this._entryArea.visible && +- !this._authList.visible) ++ !this._authList.visible && ++ !this._webLoginIntro.visible && ++ !this._webLoginDialog.visible) + this._fadeInElement(this._entryArea); } - _updateCancelButton() { -@@ -441,6 +465,51 @@ export const AuthPrompt = GObject.registerClass({ - this.emit('prompted'); - } - -+ _onWebLogin(_, serviceName, introMessage, message, url, code, buttons) { ++ _onWebLogin(_userVerifier, serviceName, introMessage, message, url, code, buttons) { + if (this._queryingService) + this.clear(); + @@ -8082,6 +8474,7 @@ index d8c7c6409d..cdad827764 100644 + this._userWell.get_child()?.showAvatar(); + this._mainBox.show(); + ++ this.webLoginActive = false; + this.remove_style_class_name('web-login-active'); + } + @@ -8090,28 +8483,17 @@ index d8c7c6409d..cdad827764 100644 + this._mainBox.hide(); + + this._webLoginDialog.update(this._webLoginParams); -+ if (!this._webLoginDialog.visible) -+ this._fadeInElement(this._webLoginDialog); ++ this._fadeInElement(this._webLoginDialog); + ++ this.webLoginActive = true; + this.add_style_class_name('web-login-active'); + } + - _onShowMessage(_userVerifier, serviceName, message, type) { - let wiggleParameters = {duration: 0}; + _onVerificationFailed(userVerifier, serviceName, canRetry) { + const wasQueryingService = this._queryingService === serviceName; -@@ -461,7 +530,9 @@ export const AuthPrompt = GObject.registerClass({ - // show the entry area to allow getting a preemptive answer - if (message && - !this._entryArea.visible && -- !this._authList.visible) -+ !this._authList.visible && -+ !this._webLoginIntro.visible && -+ !this._webLoginDialog.visible) - this._fadeInElement(this._entryArea); - - this.emit('prompted'); -@@ -487,12 +558,14 @@ export const AuthPrompt = GObject.registerClass({ - this.stopSpinning(true); +@@ -474,12 +547,14 @@ export const AuthPrompt = GObject.registerClass({ + this.stopSpinning({animate: true}); this.verificationStatus = AuthPromptStatus.VERIFICATION_SUCCEEDED; - this._mainBox.reactive = false; @@ -8131,8 +8513,8 @@ index d8c7c6409d..cdad827764 100644 }); this.emit('verification-complete'); -@@ -564,6 +637,9 @@ export const AuthPrompt = GObject.registerClass({ - stopSpinning(animate) { +@@ -551,6 +626,9 @@ export const AuthPrompt = GObject.registerClass({ + stopSpinning({animate = false} = {}) { this.emit('loading', false); this.setActorInDefaultButtonWell(this._nextButton, animate); + @@ -8141,7 +8523,7 @@ index d8c7c6409d..cdad827764 100644 } clear(params) { -@@ -581,10 +657,14 @@ export const AuthPrompt = GObject.registerClass({ +@@ -570,10 +648,14 @@ export const AuthPrompt = GObject.registerClass({ this._authListTitle.child.text = ''; this._authList.clear(); this._authList.hide(); @@ -8159,7 +8541,7 @@ index d8c7c6409d..cdad827764 100644 } setQuestion(question) { -@@ -596,6 +676,8 @@ export const AuthPrompt = GObject.registerClass({ +@@ -585,6 +667,8 @@ export const AuthPrompt = GObject.registerClass({ this._entry.hint_text = question; this._authList.hide(); @@ -8168,7 +8550,7 @@ index d8c7c6409d..cdad827764 100644 this._fadeInElement(this._entryArea); } -@@ -689,6 +771,8 @@ export const AuthPrompt = GObject.registerClass({ +@@ -678,6 +762,8 @@ export const AuthPrompt = GObject.registerClass({ updateSensitivity({sensitive}) { const authWidget = [ this._authList, @@ -8178,10 +8560,10 @@ index d8c7c6409d..cdad827764 100644 if (authWidget.reactive === sensitive) diff --git a/js/gdm/authServices.js b/js/gdm/authServices.js -index e2dbba8aff..1801fbdc3a 100644 +index 58905d15a..12c2f3ebf 100644 --- a/js/gdm/authServices.js +++ b/js/gdm/authServices.js -@@ -45,6 +45,12 @@ export class AuthServices extends GObject.Object { +@@ -49,6 +49,12 @@ export class AuthServices extends GObject.Object { param_types: [GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_JSOBJECT], }, 'mechanisms-changed': {}, @@ -8195,32 +8577,32 @@ index e2dbba8aff..1801fbdc3a 100644 static { diff --git a/js/gdm/authServicesSSSDSwitchable.js b/js/gdm/authServicesSSSDSwitchable.js -index 9649dd1382..98171b32c2 100644 +index 59f2765f6..bddcd1396 100644 --- a/js/gdm/authServicesSSSDSwitchable.js +++ b/js/gdm/authServicesSSSDSwitchable.js @@ -1,3 +1,4 @@ +import GLib from 'gi://GLib'; import GObject from 'gi://GObject'; - import * as Const from './const.js'; -@@ -15,10 +16,12 @@ export class AuthServicesSSSDSwitchable extends AuthServices { - + import * as Constants from './constants.js'; +@@ -14,10 +15,12 @@ const MechanismsStatus = { + export class AuthServicesSSSDSwitchable extends AuthServices { static SupportedRoles = [ - Const.PASSWORD_ROLE_NAME, -+ Const.WEB_LOGIN_ROLE_NAME, + Constants.PASSWORD_ROLE_NAME, ++ Constants.WEB_LOGIN_ROLE_NAME, ]; static RoleToService = { - [Const.PASSWORD_ROLE_NAME]: Const.SWITCHABLE_AUTH_SERVICE_NAME, -+ [Const.WEB_LOGIN_ROLE_NAME]: Const.SWITCHABLE_AUTH_SERVICE_NAME, + [Constants.PASSWORD_ROLE_NAME]: Constants.SWITCHABLE_AUTH_SERVICE_NAME, ++ [Constants.WEB_LOGIN_ROLE_NAME]: Constants.SWITCHABLE_AUTH_SERVICE_NAME, }; static { -@@ -58,6 +61,9 @@ export class AuthServicesSSSDSwitchable extends AuthServices { - case Const.PASSWORD_ROLE_NAME: +@@ -56,6 +59,9 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + case Constants.PASSWORD_ROLE_NAME: this._startPasswordLogin(); break; -+ case Const.WEB_LOGIN_ROLE_NAME: ++ case Constants.WEB_LOGIN_ROLE_NAME: + this._startWebLogin(); + break; } @@ -8235,7 +8617,7 @@ index 9649dd1382..98171b32c2 100644 } _handleOnCustomJSONRequest(_serviceName, _protocol, _version, json) { -@@ -130,6 +138,8 @@ export class AuthServicesSSSDSwitchable extends AuthServices { +@@ -133,6 +141,8 @@ export class AuthServicesSSSDSwitchable extends AuthServices { // filter out mechanisms with roles that are not enabled .filter(m => this._enabledRoles.includes(m.role))); @@ -8244,7 +8626,7 @@ index 9649dd1382..98171b32c2 100644 const selectedMechanism = this._enabledMechanisms .find(m => this._savedMechanism?.role === m.role) ?? -@@ -141,6 +151,31 @@ export class AuthServicesSSSDSwitchable extends AuthServices { +@@ -144,6 +154,29 @@ export class AuthServicesSSSDSwitchable extends AuthServices { this._savedMechanism = null; } @@ -8252,7 +8634,7 @@ index 9649dd1382..98171b32c2 100644 + this._clearWebLoginTimeout(); + + const webLoginMechanism = this._enabledMechanisms -+ .find(m => m.role === Const.WEB_LOGIN_ROLE_NAME); ++ .find(m => m.role === Constants.WEB_LOGIN_ROLE_NAME); + if (!webLoginMechanism) + return; + @@ -8260,34 +8642,32 @@ index 9649dd1382..98171b32c2 100644 + if (!timeout) + return; + -+ this._webLoginTimeoutId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, ++ this._webLoginTimeoutId = GLib.timeout_add_seconds_once(GLib.PRIORITY_DEFAULT, + timeout, () => { -+ if (this._selectedMechanism?.role !== Const.WEB_LOGIN_ROLE_NAME) ++ if (this._selectedMechanism?.role !== Constants.WEB_LOGIN_ROLE_NAME) + webLoginMechanism.needsRefresh = true; + else + this.emit('reset', {softReset: true}); + + this._webLoginTimeoutId = 0; -+ -+ return GLib.SOURCE_REMOVE; + }); + } + _handleOnInfo(serviceName, info) { if (!this._eventExpected()) return; -@@ -209,6 +244,10 @@ export class AuthServicesSSSDSwitchable extends AuthServices { +@@ -212,6 +245,10 @@ export class AuthServicesSSSDSwitchable extends AuthServices { response = {password: answer}; break; } -+ case Const.WEB_LOGIN_ROLE_NAME: { ++ case Constants.WEB_LOGIN_ROLE_NAME: { + response = {}; + break; + } default: throw new GObject.NotImplementedError(`formatResponse: ${role}`); } -@@ -247,4 +286,47 @@ export class AuthServicesSSSDSwitchable extends AuthServices { +@@ -250,4 +287,47 @@ export class AuthServicesSSSDSwitchable extends AuthServices { this.emit('ask-question', serviceName, prompt, true); } @@ -8318,7 +8698,7 @@ index 9649dd1382..98171b32c2 100644 + } + + _webLoginDone() { -+ if (this._selectedMechanism?.role !== Const.WEB_LOGIN_ROLE_NAME) ++ if (this._selectedMechanism?.role !== Constants.WEB_LOGIN_ROLE_NAME) + return; + + const response = this._formatResponse(); @@ -8335,10 +8715,10 @@ index 9649dd1382..98171b32c2 100644 + this._webLoginTimeoutId = 0; + } } -diff --git a/js/gdm/const.js b/js/gdm/const.js -index 2f37446c8a..e8ca48625a 100644 ---- a/js/gdm/const.js -+++ b/js/gdm/const.js +diff --git a/js/gdm/constants.js b/js/gdm/constants.js +index 2f37446c8..e8ca48625 100644 +--- a/js/gdm/constants.js ++++ b/js/gdm/constants.js @@ -3,6 +3,7 @@ export const PASSWORD_ROLE_NAME = 'password'; export const SMARTCARD_ROLE_NAME = 'smartcard'; @@ -8348,26 +8728,23 @@ 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 4c08f0957e..11b578e38f 100644 +index 69573adfe..09ec340af 100644 --- a/js/gdm/loginDialog.js +++ b/js/gdm/loginDialog.js -@@ -746,7 +746,10 @@ export const LoginDialog = GObject.registerClass({ - let authPromptAllocation = null; - let authPromptWidth = 0; - if (this._authPrompt.visible) { -- authPromptAllocation = this._getFixedTopActorAllocation(dialogBox, this._authPrompt); -+ if (this._authPrompt.has_style_class_name('web-login-active')) -+ authPromptAllocation = this._getCenterActorAllocation(dialogBox, this._authPrompt); -+ else -+ authPromptAllocation = this._getFixedTopActorAllocation(dialogBox, this._authPrompt); - authPromptWidth = authPromptAllocation.x2 - authPromptAllocation.x1; - } +@@ -45,7 +45,7 @@ import * as A11y from '../ui/status/accessibility.js'; + const _FADE_ANIMATION_TIME = 250; + const _SCROLL_ANIMATION_TIME = 500; +-const _FIXED_TOP_ACTOR_HEIGHT = 400; ++const _FIXED_TOP_ACTOR_HEIGHT = 550; + const _TIMED_LOGIN_IDLE_THRESHOLD = 5.0; + const _CONFLICTING_SESSION_DIALOG_TIMEOUT = 60; + const _PRIMARY_LOGIN_METHOD_SECTION_NAME = _('Login Options'); diff --git a/js/gdm/util.js b/js/gdm/util.js -index 36a55822cc..fe2216f8c7 100644 +index 388afbc30..b36844fbb 100644 --- a/js/gdm/util.js +++ b/js/gdm/util.js -@@ -17,6 +17,7 @@ export const PASSWORD_AUTHENTICATION_KEY = 'enable-password-authentication'; +@@ -18,6 +18,7 @@ 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'; @@ -8375,24 +8752,24 @@ index 36a55822cc..fe2216f8c7 100644 export const BANNER_MESSAGE_KEY = 'banner-message-enable'; export const BANNER_MESSAGE_SOURCE_KEY = 'banner-message-source'; export const BANNER_MESSAGE_TEXT_KEY = 'banner-message-text'; -@@ -94,6 +95,7 @@ export function isSelectable(mechanism) { +@@ -103,6 +104,7 @@ export function isSelectable(mechanism) { switch (mechanism.role) { - case Const.PASSWORD_ROLE_NAME: - case Const.SMARTCARD_ROLE_NAME: -+ case Const.WEB_LOGIN_ROLE_NAME: + case Constants.PASSWORD_ROLE_NAME: + case Constants.SMARTCARD_ROLE_NAME: ++ case Constants.WEB_LOGIN_ROLE_NAME: return true; - case Const.FINGERPRINT_ROLE_NAME: + case Constants.FINGERPRINT_ROLE_NAME: return false; -@@ -389,6 +391,8 @@ export class ShellUserVerifier extends Signals.EventEmitter { - enabledRoles.push(Const.SMARTCARD_ROLE_NAME); +@@ -416,6 +418,8 @@ export class ShellUserVerifier extends Signals.EventEmitter { + enabledRoles.push(Constants.SMARTCARD_ROLE_NAME); if (this._settings.get_boolean(FINGERPRINT_AUTHENTICATION_KEY)) - enabledRoles.push(Const.FINGERPRINT_ROLE_NAME); + enabledRoles.push(Constants.FINGERPRINT_ROLE_NAME); + if (this._settings.get_boolean(WEB_AUTHENTICATION_KEY)) -+ enabledRoles.push(Const.WEB_LOGIN_ROLE_NAME); ++ enabledRoles.push(Constants.WEB_LOGIN_ROLE_NAME); const switchableAuthentication = this._settings.get_boolean(SWITCHABLE_AUTHENTICATION_KEY); -@@ -451,6 +455,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -475,6 +479,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { 'reset', (_, ...args) => this.emit('reset', ...args), 'show-choice-list', (_, ...args) => this.emit('show-choice-list', ...args), 'mechanisms-changed', (_, ...args) => this._onMechanismsChanged(...args), @@ -8402,10 +8779,10 @@ index 36a55822cc..fe2216f8c7 100644 } diff --git a/js/gdm/webLogin.js b/js/gdm/webLogin.js new file mode 100644 -index 0000000000..69717260fc +index 000000000..8f2a0d876 --- /dev/null +++ b/js/gdm/webLogin.js -@@ -0,0 +1,310 @@ +@@ -0,0 +1,308 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- +// +// A widget showing a URL for web login @@ -8421,10 +8798,11 @@ index 0000000000..69717260fc + +const QR_CODE_SIZE = 150; +const WEB_LOGIN_SPINNER_SIZE = 35; ++const URL_LABEL_LONG_THRESHOLD = 45; + +export const WebLoginPrompt = GObject.registerClass( +class WebLoginPrompt extends St.BoxLayout { -+ _init(params) { ++ constructor(params) { + const {qrSize: qrCodeSize, message, url, code} = Params.parse(params, { + qrSize: QR_CODE_SIZE, + message: null, @@ -8432,7 +8810,7 @@ index 0000000000..69717260fc + code: null, + }); + -+ super._init({ ++ super({ + styleClass: 'web-login-prompt', + vertical: true, + y_align: Clutter.ActorAlign.CENTER, @@ -8463,6 +8841,7 @@ index 0000000000..69717260fc + this._codeBox = new St.BoxLayout({ + x_align: Clutter.ActorAlign.CENTER, + x_expand: true, ++ visible: false, + }); + + this._codeTitleLabel = new St.Label({ @@ -8475,6 +8854,7 @@ index 0000000000..69717260fc + style_class: 'web-login-code-label', + }); + this._codeBox.add_child(this._codeLabel); ++ this.add_child(this._codeBox); + + this.update({message, url, code}); + } @@ -8495,7 +8875,7 @@ index 0000000000..69717260fc + const formattedUrl = this._formatURLForDisplay(url); + this._urlLabel.text = formattedUrl; + -+ if (formattedUrl.length > 45) ++ if (formattedUrl.length > URL_LABEL_LONG_THRESHOLD) + this._urlLabel.add_style_class_name('web-login-url-label-long'); + else + this._urlLabel.remove_style_class_name('web-login-url-label-long'); @@ -8503,12 +8883,9 @@ index 0000000000..69717260fc + + if (code !== null) { + this._codeLabel.text = code; -+ if (!this._codeBox.get_parent()) -+ this.add_child(this._codeBox); ++ this._codeBox.show(); + } else { -+ // eslint-disable-next-line no-lonely-if -+ if (this._codeBox.get_parent()) -+ this.remove_child(this._codeBox); ++ this._codeBox.hide(); + } + } + @@ -8538,7 +8915,7 @@ index 0000000000..69717260fc + 'loading': {}, + }, +}, class WebLoginDialog extends St.Widget { -+ _init(params) { ++ constructor(params) { + const {message, url, code, buttons} = Params.parse(params, { + message: null, + url: null, @@ -8546,7 +8923,7 @@ index 0000000000..69717260fc + buttons: [], + }); + -+ super._init({ ++ super({ + layout_manager: new Clutter.BinLayout(), + x_expand: true, + y_expand: true, @@ -8590,9 +8967,7 @@ index 0000000000..69717260fc + } + + _updateButtons(buttons) { -+ this._buttonBox.get_children().forEach(b => b.disconnectObject(this)); -+ this._buttonBox.remove_all_children(); -+ ++ this._buttonBox.destroy_all_children(); + this._cancelButton = this._addButton({ + label: _('Cancel'), + action: () => this.emit('cancel'), @@ -8691,7 +9066,7 @@ index 0000000000..69717260fc + +export var WebLoginIntro = GObject.registerClass( +class WebLoginIntro extends St.Button { -+ _init(params) { ++ constructor(params) { + const {message} = Params.parse(params, { + message: null, + }); @@ -8701,7 +9076,7 @@ index 0000000000..69717260fc + style_class: 'web-login-button-label', + }); + -+ super._init({ ++ super({ + style_class: 'web-login-intro-button', + accessible_name: message, + button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE, @@ -8717,33 +9092,19 @@ index 0000000000..69717260fc + } +}); diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml -index 59a428df9a..85d8cc5c90 100644 +index 1da999963..ced2f28e2 100644 --- a/js/js-resources.gresource.xml +++ b/js/js-resources.gresource.xml -@@ -15,6 +15,7 @@ - gdm/realmd.js +@@ -18,6 +18,7 @@ + gdm/smartcardManager.js gdm/util.js gdm/vmware.js + gdm/webLogin.js extensions/extension.js extensions/sharedInternals.js -diff --git a/js/ui/unlockDialog.js b/js/ui/unlockDialog.js -index a16f68fee3..8a824f700a 100644 ---- a/js/ui/unlockDialog.js -+++ b/js/ui/unlockDialog.js -@@ -494,7 +494,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?.has_style_class_name('web-login-active')) { - 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 0331a1d328..e0ffd32278 100644 +index 0331a1d32..a497a3bf0 100644 --- a/js/ui/userWidget.js +++ b/js/ui/userWidget.js @@ -215,4 +215,12 @@ class UserWidget extends St.BoxLayout { @@ -8752,15 +9113,15 @@ index 0331a1d328..e0ffd32278 100644 } + + hideAvatar() { -+ this._avatar?.hide(); ++ this._avatar.hide(); + } + + showAvatar() { -+ this._avatar?.show(); ++ this._avatar.show(); + } }); diff --git a/po/POTFILES.in b/po/POTFILES.in -index d1e4d64872..9a7f8db8ea 100644 +index d1e4d6487..9a7f8db8e 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -15,6 +15,7 @@ js/gdm/authServicesLegacy.js @@ -8772,52 +9133,47 @@ index d1e4d64872..9a7f8db8ea 100644 js/misc/brightnessManager.js js/misc/systemActions.js -- -2.53.0 +2.54.0 -From 675ebdd383025c8b3a7ee76dbe81fb0f5e6353e4 Mon Sep 17 00:00:00 2001 +From 7e940666578fb759a237c568cde4d876e20c9e0c Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Tue, 14 Jan 2025 07:31:59 -0500 -Subject: [PATCH 41/42] gdm: Add support for Smartcard in +Subject: [PATCH 53/54] gdm: Add support for Smartcard in authServicesSSSDSwitchable This allows selecting smartcard as a login method. While SSSD can inform for each user what mechanisms are available, it can't know if a user has smartcard mechanism available until a valid smartcard -is inserted. To allow smartcard mechanism, if it's enabled in GDM, add an empty -smartcard mechanism for every user. +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). - -To avoid multiple events, SmartcardManager won't connect to tokens with -'/login_token', those are aliases for already connected tokens. --- js/gdm/authServicesSSSDSwitchable.js | 70 ++++++++++++++++++++++++++++ - js/misc/smartcardManager.js | 6 +++ - 2 files changed, 76 insertions(+) + 1 file changed, 70 insertions(+) diff --git a/js/gdm/authServicesSSSDSwitchable.js b/js/gdm/authServicesSSSDSwitchable.js -index 98171b32c2..b0c3583786 100644 +index bddcd1396..72335e337 100644 --- a/js/gdm/authServicesSSSDSwitchable.js +++ b/js/gdm/authServicesSSSDSwitchable.js -@@ -16,11 +16,13 @@ export class AuthServicesSSSDSwitchable extends AuthServices { - +@@ -15,11 +15,13 @@ const MechanismsStatus = { + export class AuthServicesSSSDSwitchable extends AuthServices { static SupportedRoles = [ - Const.PASSWORD_ROLE_NAME, -+ Const.SMARTCARD_ROLE_NAME, - Const.WEB_LOGIN_ROLE_NAME, + Constants.PASSWORD_ROLE_NAME, ++ Constants.SMARTCARD_ROLE_NAME, + Constants.WEB_LOGIN_ROLE_NAME, ]; static RoleToService = { - [Const.PASSWORD_ROLE_NAME]: Const.SWITCHABLE_AUTH_SERVICE_NAME, -+ [Const.SMARTCARD_ROLE_NAME]: Const.SWITCHABLE_AUTH_SERVICE_NAME, - [Const.WEB_LOGIN_ROLE_NAME]: Const.SWITCHABLE_AUTH_SERVICE_NAME, + [Constants.PASSWORD_ROLE_NAME]: Constants.SWITCHABLE_AUTH_SERVICE_NAME, ++ [Constants.SMARTCARD_ROLE_NAME]: Constants.SWITCHABLE_AUTH_SERVICE_NAME, + [Constants.WEB_LOGIN_ROLE_NAME]: Constants.SWITCHABLE_AUTH_SERVICE_NAME, }; -@@ -34,6 +36,18 @@ export class AuthServicesSSSDSwitchable extends AuthServices { +@@ -33,6 +35,18 @@ export class AuthServicesSSSDSwitchable extends AuthServices { this._mechanismsStatus = MechanismsStatus.WAITING; } @@ -8825,7 +9181,7 @@ index 98171b32c2..b0c3583786 100644 + if (serviceName !== this._selectedMechanism?.serviceName) + return; + -+ if (this._selectedMechanism.role === Const.SMARTCARD_ROLE_NAME) { ++ if (this._selectedMechanism.role === Constants.SMARTCARD_ROLE_NAME) { + const certificates = this._selectedMechanism.certificates; + const cert = certificates.find(c => c.keyId === key); + this._selectedSmartcard = cert; @@ -8833,25 +9189,25 @@ index 98171b32c2..b0c3583786 100644 + } + } + - async _handleAnswerQuery(serviceName, answer) { + _handleAnswerQuery(serviceName, answer) { if (serviceName !== this._selectedMechanism?.serviceName) return; -@@ -50,6 +64,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { +@@ -48,6 +62,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { let response; switch (this._selectedMechanism.role) { - case Const.PASSWORD_ROLE_NAME: -+ case Const.SMARTCARD_ROLE_NAME: + case Constants.PASSWORD_ROLE_NAME: ++ case Constants.SMARTCARD_ROLE_NAME: response = this._formatResponse(answer); this._sendResponse(response); break; -@@ -61,6 +76,9 @@ export class AuthServicesSSSDSwitchable extends AuthServices { - case Const.PASSWORD_ROLE_NAME: +@@ -59,6 +74,9 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + case Constants.PASSWORD_ROLE_NAME: this._startPasswordLogin(); break; -+ case Const.SMARTCARD_ROLE_NAME: ++ case Constants.SMARTCARD_ROLE_NAME: + this._startSmartcardLogin(); + break; - case Const.WEB_LOGIN_ROLE_NAME: + case Constants.WEB_LOGIN_ROLE_NAME: this._startWebLogin(); break; @@ -98,6 +116,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { @@ -8862,13 +9218,13 @@ index 98171b32c2..b0c3583786 100644 this._resettingPassword = false; -@@ -176,6 +195,14 @@ export class AuthServicesSSSDSwitchable extends AuthServices { +@@ -177,6 +196,14 @@ export class AuthServicesSSSDSwitchable extends AuthServices { }); } + _handleSmartcardChanged() { + if (!this._selectedMechanism || -+ !this._enabledMechanisms.some(({role}) => role === Const.SMARTCARD_ROLE_NAME)) ++ !this._enabledMechanisms.some(({role}) => role === Constants.SMARTCARD_ROLE_NAME)) + return; + + this.emit('reset', {softReset: true, reuseEntryText: true}); @@ -8877,19 +9233,19 @@ index 98171b32c2..b0c3583786 100644 _handleOnInfo(serviceName, info) { if (!this._eventExpected()) return; -@@ -244,6 +271,11 @@ export class AuthServicesSSSDSwitchable extends AuthServices { +@@ -245,6 +272,11 @@ export class AuthServicesSSSDSwitchable extends AuthServices { response = {password: answer}; break; } -+ case Const.SMARTCARD_ROLE_NAME: { ++ case Constants.SMARTCARD_ROLE_NAME: { + const {tokenName, moduleName, keyId, label} = this._selectedSmartcard; + response = {pin: answer, tokenName, moduleName, keyId, label}; + break; + } - case Const.WEB_LOGIN_ROLE_NAME: { + case Constants.WEB_LOGIN_ROLE_NAME: { response = {}; break; -@@ -287,6 +319,44 @@ export class AuthServicesSSSDSwitchable extends AuthServices { +@@ -288,6 +320,44 @@ export class AuthServicesSSSDSwitchable extends AuthServices { this.emit('ask-question', serviceName, prompt, true); } @@ -8934,31 +9290,14 @@ index 98171b32c2..b0c3583786 100644 _startWebLogin() { const { serviceName, -diff --git a/js/misc/smartcardManager.js b/js/misc/smartcardManager.js -index 51471e51d4..3eabd2f3be 100644 ---- a/js/misc/smartcardManager.js -+++ b/js/misc/smartcardManager.js -@@ -70,6 +70,12 @@ class SmartcardManager extends Signals.EventEmitter { - } - - _addToken(token) { -+ // This is an alias, mirroring the real token. -+ // Skip it to avoid duplicate events -+ const objectPath = token.get_object_path(); -+ if (objectPath.endsWith('/login_token')) -+ return; -+ - this._updateToken(token); - - token.connectObject('g-properties-changed', (proxy, properties) => { -- -2.53.0 +2.54.0 -From 36c26f28f6e5b1883267a12b3e76c3fd0c888cb3 Mon Sep 17 00:00:00 2001 +From 8690be303d65d0fa60fc97bcb9d98976aa19a864 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Mon, 15 Sep 2025 16:36:42 +0200 -Subject: [PATCH 42/42] gdm: Add support for Passkey in +Subject: [PATCH 54/54] gdm: Add support for Passkey in authServicesSSSDSwitchable This allows selecting passkey authentication mechanism. @@ -8970,34 +9309,36 @@ about the passkey. It's used 'show-choice-list' with an emtpy list, to display the touch instruction. --- - js/gdm/authServices.js | 12 ++++++++ + js/gdm/authServices.js | 14 +++++++++ js/gdm/authServicesSSSDSwitchable.js | 46 +++++++++++++++++++++++++++- - js/gdm/const.js | 1 + + 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, 66 insertions(+), 1 deletion(-) diff --git a/js/gdm/authServices.js b/js/gdm/authServices.js -index 1801fbdc3a..e1039485a3 100644 +index 12c2f3ebf..2726d960a 100644 --- a/js/gdm/authServices.js +++ b/js/gdm/authServices.js -@@ -2,6 +2,7 @@ - - import * as FingerprintManager from '../misc/fingerprintManager.js'; +@@ -4,6 +4,7 @@ import * as Constants from './constants.js'; + import * as FingerprintManager from './fingerprintManager.js'; import * as Params from '../misc/params.js'; -+import * as PasskeyDeviceManager from '../misc/passkeyDeviceManager.js'; - import * as SmartcardManager from '../misc/smartcardManager.js'; + import {registerDestroyableType} from '../misc/signalTracker.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'; -@@ -85,6 +86,7 @@ export class AuthServices extends GObject.Object { - this._cancellable = null; - - this._connectSmartcardManager(); -+ this._connectPasskeyDeviceManager(); - this._connectFingerprintManager(); - } - -@@ -225,6 +227,14 @@ export class AuthServices extends GObject.Object { +@@ -92,6 +93,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(); +@@ -235,6 +239,14 @@ export class AuthServices extends GObject.Object { this); } @@ -9012,7 +9353,7 @@ index 1801fbdc3a..e1039485a3 100644 _connectFingerprintManager() { // Fingerprint can only work on lockscreen if (!this._reauthOnly) -@@ -469,6 +479,8 @@ export class AuthServices extends GObject.Object { +@@ -467,6 +479,8 @@ export class AuthServices extends GObject.Object { _handleSmartcardChanged() {} @@ -9022,29 +9363,29 @@ index 1801fbdc3a..e1039485a3 100644 _handleOnInfo() {} diff --git a/js/gdm/authServicesSSSDSwitchable.js b/js/gdm/authServicesSSSDSwitchable.js -index b0c3583786..9c8464ac9d 100644 +index 72335e337..4c8eb1ae1 100644 --- a/js/gdm/authServicesSSSDSwitchable.js +++ b/js/gdm/authServicesSSSDSwitchable.js -@@ -17,12 +17,14 @@ export class AuthServicesSSSDSwitchable extends AuthServices { +@@ -16,12 +16,14 @@ export class AuthServicesSSSDSwitchable extends AuthServices { static SupportedRoles = [ - Const.PASSWORD_ROLE_NAME, - Const.SMARTCARD_ROLE_NAME, -+ Const.PASSKEY_ROLE_NAME, - Const.WEB_LOGIN_ROLE_NAME, + Constants.PASSWORD_ROLE_NAME, + Constants.SMARTCARD_ROLE_NAME, ++ Constants.PASSKEY_ROLE_NAME, + Constants.WEB_LOGIN_ROLE_NAME, ]; static RoleToService = { - [Const.PASSWORD_ROLE_NAME]: Const.SWITCHABLE_AUTH_SERVICE_NAME, - [Const.SMARTCARD_ROLE_NAME]: Const.SWITCHABLE_AUTH_SERVICE_NAME, -+ [Const.PASSKEY_ROLE_NAME]: Const.SWITCHABLE_AUTH_SERVICE_NAME, - [Const.WEB_LOGIN_ROLE_NAME]: Const.SWITCHABLE_AUTH_SERVICE_NAME, + [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, }; -@@ -68,6 +70,13 @@ export class AuthServicesSSSDSwitchable extends AuthServices { +@@ -66,6 +68,13 @@ export class AuthServicesSSSDSwitchable extends AuthServices { response = this._formatResponse(answer); this._sendResponse(response); break; -+ case Const.PASSKEY_ROLE_NAME: ++ case Constants.PASSKEY_ROLE_NAME: + response = this._formatResponse(answer); + this._sendResponse(response); + @@ -9054,23 +9395,23 @@ index b0c3583786..9c8464ac9d 100644 } } -@@ -79,6 +88,9 @@ export class AuthServicesSSSDSwitchable extends AuthServices { - case Const.SMARTCARD_ROLE_NAME: +@@ -77,6 +86,9 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + case Constants.SMARTCARD_ROLE_NAME: this._startSmartcardLogin(); break; -+ case Const.PASSKEY_ROLE_NAME: ++ case Constants.PASSKEY_ROLE_NAME: + this._startPasskeyLogin(); + break; - case Const.WEB_LOGIN_ROLE_NAME: + case Constants.WEB_LOGIN_ROLE_NAME: this._startWebLogin(); break; -@@ -203,6 +215,14 @@ export class AuthServicesSSSDSwitchable extends AuthServices { +@@ -204,6 +216,14 @@ export class AuthServicesSSSDSwitchable extends AuthServices { this.emit('reset', {softReset: true, reuseEntryText: true}); } + _handlePasskeyChanged() { + if (!this._selectedMechanism || -+ !this._enabledMechanisms.some(({role}) => role === Const.PASSKEY_ROLE_NAME)) ++ !this._enabledMechanisms.some(({role}) => role === Constants.PASSKEY_ROLE_NAME)) + return; + + this.emit('reset', {softReset: true, reuseEntryText: true}); @@ -9079,7 +9420,7 @@ index b0c3583786..9c8464ac9d 100644 _handleOnInfo(serviceName, info) { if (!this._eventExpected()) return; -@@ -263,7 +283,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { +@@ -264,7 +284,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { } _formatResponse(answer) { @@ -9088,18 +9429,18 @@ index b0c3583786..9c8464ac9d 100644 let response; switch (role) { -@@ -276,6 +296,10 @@ export class AuthServicesSSSDSwitchable extends AuthServices { +@@ -277,6 +297,10 @@ export class AuthServicesSSSDSwitchable extends AuthServices { response = {pin: answer, tokenName, moduleName, keyId, label}; break; } -+ case Const.PASSKEY_ROLE_NAME: { ++ case Constants.PASSKEY_ROLE_NAME: { + response = {pin: answer, kerberos, cryptoChallenge}; + break; + } - case Const.WEB_LOGIN_ROLE_NAME: { + case Constants.WEB_LOGIN_ROLE_NAME: { response = {}; break; -@@ -357,6 +381,26 @@ export class AuthServicesSSSDSwitchable extends AuthServices { +@@ -358,6 +382,26 @@ export class AuthServicesSSSDSwitchable extends AuthServices { }; } @@ -9126,10 +9467,10 @@ index b0c3583786..9c8464ac9d 100644 _startWebLogin() { const { serviceName, -diff --git a/js/gdm/const.js b/js/gdm/const.js -index e8ca48625a..7c48599e64 100644 ---- a/js/gdm/const.js -+++ b/js/gdm/const.js +diff --git a/js/gdm/constants.js b/js/gdm/constants.js +index e8ca48625..7c48599e6 100644 +--- a/js/gdm/constants.js ++++ b/js/gdm/constants.js @@ -3,6 +3,7 @@ export const PASSWORD_ROLE_NAME = 'password'; export const SMARTCARD_ROLE_NAME = 'smartcard'; @@ -9139,10 +9480,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 fe2216f8c7..5baa676a6a 100644 +index b36844fbb..a6339ec47 100644 --- a/js/gdm/util.js +++ b/js/gdm/util.js -@@ -16,6 +16,7 @@ export const LOGIN_SCREEN_SCHEMA = 'org.gnome.login-screen'; +@@ -17,6 +17,7 @@ export const LOGIN_SCREEN_SCHEMA = 'org.gnome.login-screen'; export const PASSWORD_AUTHENTICATION_KEY = 'enable-password-authentication'; export const FINGERPRINT_AUTHENTICATION_KEY = 'enable-fingerprint-authentication'; export const SMARTCARD_AUTHENTICATION_KEY = 'enable-smartcard-authentication'; @@ -9150,60 +9491,36 @@ index fe2216f8c7..5baa676a6a 100644 export const SWITCHABLE_AUTHENTICATION_KEY = 'enable-switchable-authentication'; export const WEB_AUTHENTICATION_KEY = 'enable-web-authentication'; export const BANNER_MESSAGE_KEY = 'banner-message-enable'; -@@ -95,6 +96,7 @@ export function isSelectable(mechanism) { +@@ -104,6 +105,7 @@ export function isSelectable(mechanism) { switch (mechanism.role) { - case Const.PASSWORD_ROLE_NAME: - case Const.SMARTCARD_ROLE_NAME: -+ case Const.PASSKEY_ROLE_NAME: - case Const.WEB_LOGIN_ROLE_NAME: + case Constants.PASSWORD_ROLE_NAME: + case Constants.SMARTCARD_ROLE_NAME: ++ case Constants.PASSKEY_ROLE_NAME: + case Constants.WEB_LOGIN_ROLE_NAME: return true; - case Const.FINGERPRINT_ROLE_NAME: -@@ -389,6 +391,8 @@ export class ShellUserVerifier extends Signals.EventEmitter { - enabledRoles.push(Const.PASSWORD_ROLE_NAME); + case Constants.FINGERPRINT_ROLE_NAME: +@@ -416,6 +418,8 @@ export class ShellUserVerifier extends Signals.EventEmitter { + enabledRoles.push(Constants.PASSWORD_ROLE_NAME); if (this._settings.get_boolean(SMARTCARD_AUTHENTICATION_KEY)) - enabledRoles.push(Const.SMARTCARD_ROLE_NAME); + enabledRoles.push(Constants.SMARTCARD_ROLE_NAME); + if (this._settings.get_boolean(PASSKEY_AUTHENTICATION_KEY)) -+ enabledRoles.push(Const.PASSKEY_ROLE_NAME); ++ enabledRoles.push(Constants.PASSKEY_ROLE_NAME); if (this._settings.get_boolean(FINGERPRINT_AUTHENTICATION_KEY)) - enabledRoles.push(Const.FINGERPRINT_ROLE_NAME); + 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 8a824f700a..16ac696029 100644 +index 50865198a..f30bf9bdf 100644 --- a/js/ui/unlockDialog.js +++ b/js/ui/unlockDialog.js -@@ -428,6 +428,8 @@ class UnlockDialogClock extends St.BoxLayout { +@@ -433,6 +433,8 @@ class UnlockDialogClock extends St.BoxLayout { - if (authMechanism?.role === GdmConst.SMARTCARD_ROLE_NAME) + if (selectedAuthRole === GdmConstants.SMARTCARD_ROLE_NAME) text = _('Insert smartcard'); -+ else if (authMechanism?.role === GdmConst.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.53.0 - -From df646fe6ac7b8739833987829c9ffc0bc31f39ef Mon Sep 17 00:00:00 2001 -From: Joan Torres Lopez -Date: Tue, 21 Apr 2026 12:57:14 +0200 -Subject: [PATCH 43/42] authPromtp: Ensure hint_text is properly cleared - ---- - js/gdm/authPrompt.js | 1 + - 1 file changed, 1 insertion(+) - -diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index cdad827..0081950 100644 ---- a/js/gdm/authPrompt.js -+++ b/js/gdm/authPrompt.js -@@ -647,6 +647,7 @@ export const AuthPrompt = GObject.registerClass({ - reuseEntryText: false, - }); - -+ this._entry.hint_text = ''; - if (!reuseEntryText) { - this._entry.text = ''; - this._inactiveEntry.text = ''; --- -2.53.0 +2.54.0 diff --git a/0001-data-Update-generated-stylesheets.patch b/0001-data-Update-generated-stylesheets.patch index 15cc6e2..20caa6a 100644 --- a/0001-data-Update-generated-stylesheets.patch +++ b/0001-data-Update-generated-stylesheets.patch @@ -4,13 +4,13 @@ Date: Tue, 16 Apr 2024 20:49:40 +0200 Subject: [PATCH] data: Update generated stylesheets --- - data/theme/gnome-shell-dark.css | 468 +++++++++++++++++----- - data/theme/gnome-shell-high-contrast.css | 484 ++++++++++++++++++----- - data/theme/gnome-shell-light.css | 467 +++++++++++++++++----- - 3 files changed, 1095 insertions(+), 324 deletions(-) + data/theme/gnome-shell-dark.css | 492 +++++++++++++++++----- + data/theme/gnome-shell-high-contrast.css | 508 ++++++++++++++++++----- + data/theme/gnome-shell-light.css | 491 +++++++++++++++++----- + 3 files changed, 1161 insertions(+), 330 deletions(-) diff --git a/data/theme/gnome-shell-dark.css b/data/theme/gnome-shell-dark.css -index 82e68c5..a96fde1 100644 +index 82e68c5..e112db8 100644 --- a/data/theme/gnome-shell-dark.css +++ b/data/theme/gnome-shell-dark.css @@ -42,7 +42,9 @@ stage { @@ -567,23 +567,38 @@ index 82e68c5..a96fde1 100644 .login-dialog, .unlock-dialog { color: #fafafb; } -@@ -3044,19 +3092,62 @@ StScrollBar { +@@ -3042,21 +3090,88 @@ StScrollBar { + color: #fafafb; } + .login-dialog .login-dialog-prompt-layout, .unlock-dialog .login-dialog-prompt-layout { - width: 25em; - spacing: 9px; } +- width: 25em; ++ width: 30em; ++ margin-top: 80px; } + .login-dialog .login-dialog-prompt-layout.web-login-active, + .unlock-dialog .login-dialog-prompt-layout.web-login-active { -+ width: 37.5em; } ++ width: 37.5em; ++ margin-top: 0; } ++ .login-dialog .login-dialog-input-well, ++ .unlock-dialog .login-dialog-input-well { + spacing: 9px; } + .login-dialog .login-dialog-prompt-entry-area, + .unlock-dialog .login-dialog-prompt-entry-area { + margin: 0.5em 20px; } + .login-dialog .login-dialog-prompt-entry, + .unlock-dialog .login-dialog-prompt-entry { -+ border-radius: 12px; -+ padding-right: 3em; } -+ .login-dialog .login-dialog-default-button-well, -+ .unlock-dialog .login-dialog-default-button-well { -+ margin-right: 1em; } ++ 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-bottom-button-group { padding: 32px; @@ -599,33 +614,45 @@ index 82e68c5..a96fde1 100644 + .login-dialog-button.cancel-button { - padding: 9px; } -+ padding: 12px; } ++ padding: 12px; ++ margin: 0.5em 0; } + +.login-dialog-auth-menu-button-popup { + padding: 18px; + margin-right: 12px; } -+ .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-header { -+ font-size: 0.909em; -+ text-align: center; -+ font-weight: bold; -+ padding-top: 18px; -+ padding-bottom: 6px; } -+ .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-header:first-child { -+ padding-top: 6px; } -+ .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-indicator { ++ .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-section { ++ padding-top: 6px; } ++ .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-section:first-child { ++ padding-top: 0; } ++ .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-section-label { ++ font-size: 0.909em; ++ font-weight: bold; ++ color: #fafafb; } ++ .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-icon { ++ color: #fafafb; ++ icon-size: 1.091em; ++ margin-right: 8px; } ++ .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-box { + spacing: 3px; } -+ .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-indicator .login-dialog-auth-menu-item-indicator-name { -+ font-size: 1.159em; ++ .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-box-name { ++ color: #fafafb; ++ font-size: 1em; + font-weight: bold; } -+ .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-indicator .login-dialog-auth-menu-item-indicator-description { -+ font-size: 0.977em; } ++ .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-box-description { ++ color: #fafafb; ++ font-size: 0.909em; } + +.login-dialog-auth-menu-button-indicator { -+ background-color: transparent !important; } ++ background-color: transparent !important; ++ 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: 1em; ++ font-weight: normal; ++ margin-left: 12px; } .login-dialog-button-box { - spacing: 12px; } @@ -633,7 +660,7 @@ index 82e68c5..a96fde1 100644 .conflicting-session-dialog-content { spacing: 20px; } -@@ -3118,49 +3209,104 @@ StScrollBar { +@@ -3118,49 +3233,104 @@ StScrollBar { background-color: st-mix(st-mix(-st-accent-color, #ffffff, 60%), st-lighten(#222226, 9%), 5%); } .login-dialog-auth-list-view { @@ -703,8 +730,8 @@ index 82e68c5..a96fde1 100644 +.login-dialog .login-dialog-auth-list-item { + min-height: 3em; + padding: 9px; -+ margin: 0 20px; -+ margin-bottom: 4px; } ++ margin-bottom: 4px; ++ margin-right: 4px; } .unlock-dialog .login-dialog-auth-list-item { - border-radius: 9.6px; @@ -712,29 +739,26 @@ index 82e68c5..a96fde1 100644 + border-radius: 12px; + min-height: 3em; + padding: 9px; -+ margin: 0 20px; -+ margin-bottom: 4px; } ++ margin-bottom: 4px; ++ margin-right: 4px; } + +.unlock-dialog .login-dialog-auth-list-title { + background-color: transparent !important; + color: #fafafb !important; + padding: 0; + margin: 0; } - --.login-dialog-auth-list-label:ltr { -- padding-left: 15px; -- text-align: left; } ++ +.login-dialog-auth-list-title-label { + padding: 6px; + text-align: center; } --.login-dialog-auth-list-label:rtl { -- padding-right: 15px; -- text-align: right; } +-.login-dialog-auth-list-label:ltr { +- padding-left: 15px; +- text-align: left; } +.login-dialog-auth-list-item-title, +.login-dialog-auth-list-item-subtitle { + text-align: center; -+ padding: 1.8px 0; } ++ padding: 1.8px 2em; } + +.login-dialog-auth-list-item-title { + color: #fafafb; } @@ -742,7 +766,10 @@ index 82e68c5..a96fde1 100644 +.login-dialog-auth-list-item-subtitle { + color: #c1c1ce; + font-weight: 500; } -+ + +-.login-dialog-auth-list-label:rtl { +- padding-right: 15px; +- text-align: right; } +.login-dialog-item-icon { + width: 1.3em; + height: 1.3em; @@ -764,7 +791,7 @@ index 82e68c5..a96fde1 100644 .login-dialog-user-list-view { width: 25em; -@@ -3189,6 +3335,9 @@ StScrollBar { +@@ -3189,6 +3359,9 @@ StScrollBar { background-color: st-lighten(st-lighten(st-mix(#fafafb, #222226, 9%), 9%), 4%); } .login-dialog-user-list-view .login-dialog-user-list .login-dialog-user-list-item:active:focus { background-color: st-mix(st-mix(-st-accent-color, #ffffff, 60%), st-lighten(st-mix(#fafafb, #222226, 9%), 9%), 5%); } @@ -774,7 +801,7 @@ index 82e68c5..a96fde1 100644 .login-dialog-user-list-view .login-dialog-user-list .login-dialog-user-list-item .user-icon { border: 2px solid transparent; } .login-dialog-user-list-view .login-dialog-user-list .login-dialog-user-list-item .login-dialog-timed-login-indicator { -@@ -3200,6 +3349,102 @@ StScrollBar { +@@ -3200,6 +3373,104 @@ StScrollBar { .login-dialog-user-list-view .login-dialog-user-list .login-dialog-user-list-item:logged-in .user-icon StIcon { background-color: st-transparentize(-st-accent-color, 0.7); } @@ -855,7 +882,8 @@ index 82e68c5..a96fde1 100644 + background-color: st-darken(st-mix(#fafafb, #222226, 9%), 3%); } + +.login-dialog .web-login-intro-button { -+ padding: 0; } ++ padding: 0; ++ margin: 0.2em 20px; } + +.login-dialog .web-login-prompt-button { + padding: 15px 24px; @@ -867,7 +895,8 @@ index 82e68c5..a96fde1 100644 + border-radius: 32px; } + +.unlock-dialog .web-login-intro-button { -+ padding: 0; } ++ padding: 0; ++ margin: 0.2em 20px; } + +.unlock-dialog .web-login-prompt-button { + padding: 15px 24px; @@ -877,19 +906,18 @@ index 82e68c5..a96fde1 100644 .unlock-dialog { background-color: transparent; } -@@ -3308,3 +3553,10 @@ StScrollBar { - .login-dialog .user-widget.vertical .user-icon StIcon, - .unlock-dialog .user-widget.vertical .user-icon StIcon { - padding: 30px; } -+ -+.qr-code { -+ border-radius: 4px; -+ border-width: 1em; -+ background-color: #fafafb; -+ border-color: #fafafb; -+ color: #222226; } +@@ -3212,7 +3483,8 @@ StScrollBar { + + .unlock-dialog-clock { + color: #fafafb; +- spacing: 2em; } ++ spacing: 2em; ++ margin-top: 150px; } + .unlock-dialog-clock .unlock-dialog-clock-time { + font-size: 6.546em; + font-weight: 800; } diff --git a/data/theme/gnome-shell-high-contrast.css b/data/theme/gnome-shell-high-contrast.css -index b69823f..0ed28a3 100644 +index b69823f..bc41454 100644 --- a/data/theme/gnome-shell-high-contrast.css +++ b/data/theme/gnome-shell-high-contrast.css @@ -42,7 +42,9 @@ stage { @@ -1498,23 +1526,38 @@ index b69823f..0ed28a3 100644 .login-dialog, .unlock-dialog { color: #ffffff; } -@@ -3374,19 +3422,62 @@ StScrollBar { +@@ -3372,21 +3420,88 @@ StScrollBar { + color: #ffffff; } + .login-dialog .login-dialog-prompt-layout, .unlock-dialog .login-dialog-prompt-layout { - width: 25em; - spacing: 9px; } +- width: 25em; ++ width: 30em; ++ margin-top: 80px; } + .login-dialog .login-dialog-prompt-layout.web-login-active, + .unlock-dialog .login-dialog-prompt-layout.web-login-active { -+ width: 37.5em; } ++ width: 37.5em; ++ margin-top: 0; } ++ .login-dialog .login-dialog-input-well, ++ .unlock-dialog .login-dialog-input-well { + spacing: 9px; } + .login-dialog .login-dialog-prompt-entry-area, + .unlock-dialog .login-dialog-prompt-entry-area { + margin: 0.5em 20px; } + .login-dialog .login-dialog-prompt-entry, + .unlock-dialog .login-dialog-prompt-entry { -+ border-radius: 12px; -+ padding-right: 3em; } -+ .login-dialog .login-dialog-default-button-well, -+ .unlock-dialog .login-dialog-default-button-well { -+ margin-right: 1em; } ++ 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-bottom-button-group { padding: 32px; @@ -1530,33 +1573,45 @@ index b69823f..0ed28a3 100644 + .login-dialog-button.cancel-button { - padding: 9px; } -+ padding: 12px; } ++ padding: 12px; ++ margin: 0.5em 0; } + +.login-dialog-auth-menu-button-popup { + padding: 18px; + margin-right: 12px; } -+ .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-header { -+ font-size: 0.909em; -+ text-align: center; -+ font-weight: bold; -+ padding-top: 18px; -+ padding-bottom: 6px; } -+ .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-header:first-child { -+ padding-top: 6px; } -+ .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-indicator { ++ .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-section { ++ padding-top: 6px; } ++ .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-section:first-child { ++ padding-top: 0; } ++ .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-section-label { ++ font-size: 0.909em; ++ font-weight: bold; ++ color: #ffffff; } ++ .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-icon { ++ color: #ffffff; ++ icon-size: 1.091em; ++ margin-right: 8px; } ++ .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-box { + spacing: 3px; } -+ .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-indicator .login-dialog-auth-menu-item-indicator-name { -+ font-size: 1.159em; ++ .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-box-name { ++ color: #ffffff; ++ font-size: 1em; + font-weight: bold; } -+ .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-indicator .login-dialog-auth-menu-item-indicator-description { -+ font-size: 0.977em; } ++ .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-box-description { ++ color: #ffffff; ++ font-size: 0.909em; } + +.login-dialog-auth-menu-button-indicator { -+ background-color: transparent !important; } ++ background-color: transparent !important; ++ 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: 1em; ++ font-weight: normal; ++ margin-left: 12px; } .login-dialog-button-box { - spacing: 12px; } @@ -1564,7 +1619,7 @@ index b69823f..0ed28a3 100644 .conflicting-session-dialog-content { spacing: 20px; } -@@ -3455,53 +3546,112 @@ StScrollBar { +@@ -3455,53 +3570,112 @@ StScrollBar { background-color: st-mix(st-mix(-st-accent-color, #ffffff, 60%), st-mix(st-lighten(#000000, 9%), #ffffff, 87%), 5%); } .login-dialog-auth-list-view { @@ -1642,8 +1697,8 @@ index b69823f..0ed28a3 100644 +.login-dialog .login-dialog-auth-list-item { + min-height: 3em; + padding: 9px; -+ margin: 0 20px; -+ margin-bottom: 4px; } ++ margin-bottom: 4px; ++ margin-right: 4px; } .unlock-dialog .login-dialog-auth-list-item { - border-radius: 9.6px; @@ -1651,15 +1706,18 @@ index b69823f..0ed28a3 100644 + border-radius: 12px; + min-height: 3em; + padding: 9px; -+ margin: 0 20px; -+ margin-bottom: 4px; } ++ margin-bottom: 4px; ++ margin-right: 4px; } + +.unlock-dialog .login-dialog-auth-list-title { + background-color: transparent !important; + color: #ffffff !important; + padding: 0; + margin: 0; } -+ + +-.login-dialog-auth-list-label:ltr { +- padding-left: 15px; +- text-align: left; } +.login-dialog-auth-list-title-label { + padding: 6px; + text-align: center; } @@ -1667,17 +1725,14 @@ index b69823f..0ed28a3 100644 +.login-dialog-auth-list-item-title, +.login-dialog-auth-list-item-subtitle { + text-align: center; -+ padding: 1.8px 0; } - --.login-dialog-auth-list-label:ltr { -- padding-left: 15px; -- text-align: left; } -+.login-dialog-auth-list-item-title { -+ color: #ffffff; } ++ padding: 1.8px 2em; } -.login-dialog-auth-list-label:rtl { - padding-right: 15px; - text-align: right; } ++.login-dialog-auth-list-item-title { ++ color: #ffffff; } ++ +.login-dialog-auth-list-item-subtitle { + color: #cccccc; + font-weight: 500; } @@ -1703,7 +1758,7 @@ index b69823f..0ed28a3 100644 .login-dialog-user-list-view { width: 25em; -@@ -3534,6 +3684,13 @@ StScrollBar { +@@ -3534,6 +3708,13 @@ StScrollBar { background-color: st-lighten(st-lighten(st-mix(#ffffff, #000000, 9%), 9%), 4%); } .login-dialog-user-list-view .login-dialog-user-list .login-dialog-user-list-item:active:focus { background-color: st-mix(st-mix(-st-accent-color, #ffffff, 60%), st-mix(st-lighten(st-mix(#ffffff, #000000, 9%), 9%), #ffffff, 87%), 5%); } @@ -1717,7 +1772,7 @@ index b69823f..0ed28a3 100644 .login-dialog-user-list-view .login-dialog-user-list .login-dialog-user-list-item .user-icon { border: 2px solid transparent; } .login-dialog-user-list-view .login-dialog-user-list .login-dialog-user-list-item .login-dialog-timed-login-indicator { -@@ -3545,6 +3702,110 @@ StScrollBar { +@@ -3545,6 +3726,112 @@ StScrollBar { .login-dialog-user-list-view .login-dialog-user-list .login-dialog-user-list-item:logged-in .user-icon StIcon { background-color: st-transparentize(-st-accent-color, 0.7); } @@ -1806,7 +1861,8 @@ index b69823f..0ed28a3 100644 + border: none; } + +.login-dialog .web-login-intro-button { -+ padding: 0; } ++ padding: 0; ++ margin: 0.2em 20px; } + +.login-dialog .web-login-prompt-button { + padding: 15px 24px; @@ -1818,7 +1874,8 @@ index b69823f..0ed28a3 100644 + border-radius: 32px; } + +.unlock-dialog .web-login-intro-button { -+ padding: 0; } ++ padding: 0; ++ margin: 0.2em 20px; } + +.unlock-dialog .web-login-prompt-button { + padding: 15px 24px; @@ -1828,19 +1885,18 @@ index b69823f..0ed28a3 100644 .unlock-dialog { background-color: transparent; } -@@ -3655,3 +3916,10 @@ StScrollBar { - .login-dialog .user-widget.vertical .user-icon StIcon, - .unlock-dialog .user-widget.vertical .user-icon StIcon { - padding: 30px; } -+ -+.qr-code { -+ border-radius: 4px; -+ border-width: 1em; -+ background-color: #ffffff; -+ border-color: #ffffff; -+ color: #000000; } +@@ -3557,7 +3844,8 @@ StScrollBar { + + .unlock-dialog-clock { + color: #ffffff; +- spacing: 2em; } ++ spacing: 2em; ++ margin-top: 150px; } + .unlock-dialog-clock .unlock-dialog-clock-time { + font-size: 6.546em; + font-weight: 800; } diff --git a/data/theme/gnome-shell-light.css b/data/theme/gnome-shell-light.css -index 2b67846..792690e 100644 +index 2b67846..c3288bb 100644 --- a/data/theme/gnome-shell-light.css +++ b/data/theme/gnome-shell-light.css @@ -42,7 +42,9 @@ stage { @@ -2396,23 +2452,38 @@ index 2b67846..792690e 100644 .login-dialog, .unlock-dialog { color: #fafafb; } -@@ -3045,19 +3092,62 @@ StScrollBar { +@@ -3043,21 +3090,88 @@ StScrollBar { + color: #fafafb; } + .login-dialog .login-dialog-prompt-layout, .unlock-dialog .login-dialog-prompt-layout { - width: 25em; - spacing: 9px; } +- width: 25em; ++ width: 30em; ++ margin-top: 80px; } + .login-dialog .login-dialog-prompt-layout.web-login-active, + .unlock-dialog .login-dialog-prompt-layout.web-login-active { -+ width: 37.5em; } ++ width: 37.5em; ++ margin-top: 0; } ++ .login-dialog .login-dialog-input-well, ++ .unlock-dialog .login-dialog-input-well { + spacing: 9px; } + .login-dialog .login-dialog-prompt-entry-area, + .unlock-dialog .login-dialog-prompt-entry-area { + margin: 0.5em 20px; } + .login-dialog .login-dialog-prompt-entry, + .unlock-dialog .login-dialog-prompt-entry { -+ border-radius: 12px; -+ padding-right: 3em; } -+ .login-dialog .login-dialog-default-button-well, -+ .unlock-dialog .login-dialog-default-button-well { -+ margin-right: 1em; } ++ 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-bottom-button-group { padding: 32px; @@ -2428,33 +2499,45 @@ index 2b67846..792690e 100644 + .login-dialog-button.cancel-button { - padding: 9px; } -+ padding: 12px; } ++ padding: 12px; ++ margin: 0.5em 0; } + +.login-dialog-auth-menu-button-popup { + padding: 18px; + margin-right: 12px; } -+ .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-header { -+ font-size: 0.909em; -+ text-align: center; -+ font-weight: bold; -+ padding-top: 18px; -+ padding-bottom: 6px; } -+ .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-header:first-child { -+ padding-top: 6px; } -+ .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-indicator { ++ .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-section { ++ padding-top: 6px; } ++ .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-section:first-child { ++ padding-top: 0; } ++ .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-section-label { ++ font-size: 0.909em; ++ font-weight: bold; ++ color: #fafafb; } ++ .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-icon { ++ color: #fafafb; ++ icon-size: 1.091em; ++ margin-right: 8px; } ++ .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-box { + spacing: 3px; } -+ .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-indicator .login-dialog-auth-menu-item-indicator-name { -+ font-size: 1.159em; ++ .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-box-name { ++ color: #fafafb; ++ font-size: 1em; + font-weight: bold; } -+ .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-indicator .login-dialog-auth-menu-item-indicator-description { -+ font-size: 0.977em; } ++ .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-box-description { ++ color: #fafafb; ++ font-size: 0.909em; } + +.login-dialog-auth-menu-button-indicator { -+ background-color: transparent !important; } ++ background-color: transparent !important; ++ 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: 1em; ++ font-weight: normal; ++ margin-left: 12px; } .login-dialog-button-box { - spacing: 12px; } @@ -2462,7 +2545,7 @@ index 2b67846..792690e 100644 .conflicting-session-dialog-content { spacing: 20px; } -@@ -3119,49 +3209,104 @@ StScrollBar { +@@ -3119,49 +3233,104 @@ StScrollBar { background-color: st-mix(-st-accent-color, st-lighten(#222226, 9%), 5%); } .login-dialog-auth-list-view { @@ -2532,8 +2615,8 @@ index 2b67846..792690e 100644 +.login-dialog .login-dialog-auth-list-item { + min-height: 3em; + padding: 9px; -+ margin: 0 20px; -+ margin-bottom: 4px; } ++ margin-bottom: 4px; ++ margin-right: 4px; } .unlock-dialog .login-dialog-auth-list-item { - border-radius: 9.6px; @@ -2541,18 +2624,21 @@ index 2b67846..792690e 100644 + border-radius: 12px; + min-height: 3em; + padding: 9px; -+ margin: 0 20px; -+ margin-bottom: 4px; } -+ ++ margin-bottom: 4px; ++ margin-right: 4px; } + +-.login-dialog-auth-list-label:ltr { +- padding-left: 15px; +- text-align: left; } +.unlock-dialog .login-dialog-auth-list-title { + background-color: transparent !important; + color: #fafafb !important; + padding: 0; + margin: 0; } --.login-dialog-auth-list-label:ltr { -- padding-left: 15px; -- text-align: left; } +-.login-dialog-auth-list-label:rtl { +- padding-right: 15px; +- text-align: right; } +.login-dialog-auth-list-title-label { + padding: 6px; + text-align: center; } @@ -2560,14 +2646,11 @@ index 2b67846..792690e 100644 +.login-dialog-auth-list-item-title, +.login-dialog-auth-list-item-subtitle { + text-align: center; -+ padding: 1.8px 0; } ++ padding: 1.8px 2em; } + +.login-dialog-auth-list-item-title { + color: #fafafb; } - --.login-dialog-auth-list-label:rtl { -- padding-right: 15px; -- text-align: right; } ++ +.login-dialog-auth-list-item-subtitle { + color: #c1c1ce; + font-weight: 500; } @@ -2593,7 +2676,7 @@ index 2b67846..792690e 100644 .login-dialog-user-list-view { width: 25em; -@@ -3190,6 +3335,9 @@ StScrollBar { +@@ -3190,6 +3359,9 @@ StScrollBar { background-color: st-lighten(st-lighten(st-mix(#fafafb, #222226, 12%), 9%), 4%); } .login-dialog-user-list-view .login-dialog-user-list .login-dialog-user-list-item:active:focus { background-color: st-mix(-st-accent-color, st-lighten(st-mix(#fafafb, #222226, 12%), 9%), 5%); } @@ -2603,7 +2686,7 @@ index 2b67846..792690e 100644 .login-dialog-user-list-view .login-dialog-user-list .login-dialog-user-list-item .user-icon { border: 2px solid transparent; } .login-dialog-user-list-view .login-dialog-user-list .login-dialog-user-list-item .login-dialog-timed-login-indicator { -@@ -3201,6 +3349,102 @@ StScrollBar { +@@ -3201,6 +3373,104 @@ StScrollBar { .login-dialog-user-list-view .login-dialog-user-list .login-dialog-user-list-item:logged-in .user-icon StIcon { background-color: st-transparentize(-st-accent-color, 0.7); } @@ -2684,7 +2767,8 @@ index 2b67846..792690e 100644 + background-color: st-darken(st-mix(#fafafb, #222226, 12%), 3%); } + +.login-dialog .web-login-intro-button { -+ padding: 0; } ++ padding: 0; ++ margin: 0.2em 20px; } + +.login-dialog .web-login-prompt-button { + padding: 15px 24px; @@ -2696,7 +2780,8 @@ index 2b67846..792690e 100644 + border-radius: 32px; } + +.unlock-dialog .web-login-intro-button { -+ padding: 0; } ++ padding: 0; ++ margin: 0.2em 20px; } + +.unlock-dialog .web-login-prompt-button { + padding: 15px 24px; @@ -2706,17 +2791,16 @@ index 2b67846..792690e 100644 .unlock-dialog { background-color: transparent; } -@@ -3309,3 +3553,10 @@ StScrollBar { - .login-dialog .user-widget.vertical .user-icon StIcon, - .unlock-dialog .user-widget.vertical .user-icon StIcon { - padding: 30px; } -+ -+.qr-code { -+ border-radius: 4px; -+ border-width: 1em; -+ background-color: #e9e9ea; -+ border-color: #e9e9ea; -+ color: #222226; } +@@ -3213,7 +3483,8 @@ StScrollBar { + + .unlock-dialog-clock { + color: #fafafb; +- spacing: 2em; } ++ spacing: 2em; ++ margin-top: 150px; } + .unlock-dialog-clock .unlock-dialog-clock-time { + font-size: 6.546em; + font-weight: 800; } -- -2.53.0 +2.54.0 diff --git a/0001-gdm-Work-around-failing-fingerprint-auth.patch b/0001-gdm-Work-around-failing-fingerprint-auth.patch index 30d2260..92b6768 100644 --- a/0001-gdm-Work-around-failing-fingerprint-auth.patch +++ b/0001-gdm-Work-around-failing-fingerprint-auth.patch @@ -9,36 +9,38 @@ the PAM configuration has not been updated and no prints are enrolled. So, consider a verification failure within one second to be a service failure instead. --- - js/gdm/authServicesLegacy.js | 44 +++++++++++++++++++++++++----------- - 1 file changed, 31 insertions(+), 13 deletions(-) + js/gdm/authServicesLegacy.js | 47 ++++++++++++++++++++++++------------ + 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/js/gdm/authServicesLegacy.js b/js/gdm/authServicesLegacy.js -index 0813622a01..c85a9572f5 100644 +index 7b5739f14..99c034f64 100644 --- a/js/gdm/authServicesLegacy.js +++ b/js/gdm/authServicesLegacy.js -@@ -58,6 +58,7 @@ export class AuthServicesLegacy extends AuthServices { - this._addCredentialManager(Vmware.SERVICE_NAME, Vmware.getVmwareCredentialsManager()); +@@ -57,6 +57,7 @@ export class AuthServicesLegacy extends AuthServices { + this.addCredentialManager(Vmware.SERVICE_NAME, Vmware.getVmwareCredentialsManager()); this._fingerprintReadyTimeoutId = 0; + this._fprintStartTime = -1; } _handleSelectChoice(serviceName, key) { -@@ -117,16 +118,20 @@ export class AuthServicesLegacy extends AuthServices { +@@ -114,18 +115,21 @@ export class AuthServicesLegacy extends AuthServices { } _handleOnConversationStarted(serviceName) { -- if (serviceName === Const.FINGERPRINT_SERVICE_NAME && -- this._fingerprintReadyTimeoutId === 0) { -- this._fingerprintReadyTimeoutId = GLib.timeout_add( -- GLib.PRIORITY_DEFAULT, -- FINGERPRINT_READY_TIMEOUT_MS, -- () => { -- this._fingerprintReadyTimeoutId = 0; -- this._setFingerprintReady(); -- return GLib.SOURCE_REMOVE; -- }); -+ if (serviceName === Const.FINGERPRINT_SERVICE_NAME) { +- if (serviceName !== Constants.FINGERPRINT_SERVICE_NAME || +- this._fingerprintReadyTimeoutId !== 0) +- return; +- +- this._fingerprintReadyTimeoutId = GLib.timeout_add( +- GLib.PRIORITY_DEFAULT, +- FINGERPRINT_READY_TIMEOUT_MS, +- () => { +- this._fingerprintReadyTimeoutId = 0; +- this._setFingerprintReady(true); +- return GLib.SOURCE_REMOVE; +- }); ++ if (serviceName === Constants.FINGERPRINT_SERVICE_NAME) { + this._fprintStartTime = GLib.get_monotonic_time(); + + @@ -48,14 +50,15 @@ index 0813622a01..c85a9572f5 100644 + FINGERPRINT_READY_TIMEOUT_MS, + () => { + this._fingerprintReadyTimeoutId = 0; -+ this._setFingerprintReady(); ++ this._setFingerprintReady(true); + return GLib.SOURCE_REMOVE; + }); + } - } ++ } } -@@ -234,6 +239,7 @@ export class AuthServicesLegacy extends AuthServices { + _setFingerprintReady(ready) { +@@ -231,6 +235,7 @@ export class AuthServicesLegacy extends AuthServices { this._fingerprintFailedId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, FINGERPRINT_ERROR_TIMEOUT_WAIT, () => { @@ -63,10 +66,10 @@ index 0813622a01..c85a9572f5 100644 this._fingerprintFailedId = 0; if (!this._cancellable.is_cancelled()) this._verificationFailed(serviceName, false); -@@ -314,9 +320,21 @@ export class AuthServicesLegacy extends AuthServices { +@@ -312,9 +317,21 @@ export class AuthServicesLegacy extends AuthServices { _handleVerificationFailed(serviceName) { - if (serviceName === Const.FINGERPRINT_SERVICE_NAME && + if (serviceName === Constants.FINGERPRINT_SERVICE_NAME && - this._enabledMechanisms.some(m => m.serviceName === serviceName) && - this._fingerprintFailedId) - GLib.source_remove(this._fingerprintFailedId); @@ -89,5 +92,5 @@ index 0813622a01..c85a9572f5 100644 _handleOnVerificationComplete(serviceName) { -- -2.53.0 +2.54.0 diff --git a/0002-post-changes-for-passwordless-gdm-backport.patch b/0002-post-changes-for-passwordless-gdm-backport.patch new file mode 100644 index 0000000..f25a593 --- /dev/null +++ b/0002-post-changes-for-passwordless-gdm-backport.patch @@ -0,0 +1,163 @@ +From 5b24694b73b58a77db1e017d2c97bfc673236572 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Tue, 2 Jun 2026 11:43:10 +0200 +Subject: [PATCH] Change inputWell by this + +In this version inputWell doesn't exist yet. +--- + js/gdm/authPrompt.js | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index e511b3881..3f2765f9c 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -196,7 +196,7 @@ export const AuthPrompt = GObject.registerClass({ + }, + }); + }); +- this._inputWell.add_child(this._authList); ++ this.add_child(this._authList); + + // Use an insensitive button for the auth list title + // to get the same style as the auth list buttons +@@ -315,7 +315,7 @@ export const AuthPrompt = GObject.registerClass({ + } + }); + this._webLoginDialog.connect('loading', () => this.emit('loading', this._webLoginDialog.isLoading)); +- this._inputWell.add_child(this._webLoginDialog); ++ this.add_child(this._webLoginDialog); + + // center elements inside _mainBox between the cancel + // button on the left and this spacer on the right +-- +2.54.0 + +From 848071303afba7aa58486b643e5631a0d75d40b9 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Tue, 2 Jun 2026 12:07:45 +0200 +Subject: [PATCH] Use Object.keys(this._sections) instead of + this._sections.keys() + +--- + js/gdm/authMenuButton.js | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/js/gdm/authMenuButton.js b/js/gdm/authMenuButton.js +index cbc1d7676..6a697cbc2 100644 +--- a/js/gdm/authMenuButton.js ++++ b/js/gdm/authMenuButton.js +@@ -252,7 +252,7 @@ export class AuthMenuButton extends St.Button { + this._items.delete(itemKey); + }); + +- this._sections.keys().forEach(sectionName => { ++ Object.keys(this._sections).forEach(sectionName => { + const itemsInSection = this._findItems({sectionName}); + if (itemsInSection.length === 0) { + const section = this._sections.get(sectionName); +-- +2.54.0 + +From 8a987a7b32a1e3edbb04f9a9ff3077fac7d6a151 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Tue, 2 Jun 2026 12:52:09 +0200 +Subject: [PATCH] Don't use _once timeout variants + +--- + js/gdm/authServices.js | 7 +++++-- + js/gdm/authServicesLegacy.js | 3 ++- + js/gdm/authServicesSSSDSwitchable.js | 3 ++- + 3 files changed, 9 insertions(+), 4 deletions(-) + +diff --git a/js/gdm/authServices.js b/js/gdm/authServices.js +index 2726d960a..410f0bc16 100644 +--- a/js/gdm/authServices.js ++++ b/js/gdm/authServices.js +@@ -260,8 +260,11 @@ export class AuthServices extends GObject.Object { + + _waitPendingMessages() { + const cancellable = this._cancellable; +- const timeoutId = GLib.timeout_add_seconds_once(GLib.PRIORITY_DEFAULT, 10, +- () => cancellable.cancel()); ++ const timeoutId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 10, ++ () => { ++ cancellable.cancel(); ++ return GLib.SOURCE_REMOVE; ++ }); + + const {promise, resolve, reject} = Promise.withResolvers(); + const task = Gio.Task.new(this, cancellable, () => { +diff --git a/js/gdm/authServicesLegacy.js b/js/gdm/authServicesLegacy.js +index 132c5307c..7b5739f14 100644 +--- a/js/gdm/authServicesLegacy.js ++++ b/js/gdm/authServicesLegacy.js +@@ -118,12 +118,13 @@ export class AuthServicesLegacy extends AuthServices { + this._fingerprintReadyTimeoutId !== 0) + return; + +- this._fingerprintReadyTimeoutId = GLib.timeout_add_once( ++ this._fingerprintReadyTimeoutId = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, + FINGERPRINT_READY_TIMEOUT_MS, + () => { + this._fingerprintReadyTimeoutId = 0; + this._setFingerprintReady(true); ++ return GLib.SOURCE_REMOVE; + }); + } + +diff --git a/js/gdm/authServicesSSSDSwitchable.js b/js/gdm/authServicesSSSDSwitchable.js +index 4c8eb1ae1..f98c86e09 100644 +--- a/js/gdm/authServicesSSSDSwitchable.js ++++ b/js/gdm/authServicesSSSDSwitchable.js +@@ -197,7 +197,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + if (!timeout) + return; + +- this._webLoginTimeoutId = GLib.timeout_add_seconds_once(GLib.PRIORITY_DEFAULT, ++ this._webLoginTimeoutId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, + timeout, () => { + if (this._selectedMechanism?.role !== Constants.WEB_LOGIN_ROLE_NAME) + webLoginMechanism.needsRefresh = true; +@@ -205,6 +205,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + this.emit('reset', {softReset: true}); + + this._webLoginTimeoutId = 0; ++ return GLib.SOURCE_REMOVE; + }); + } + +-- +2.54.0 + +From 0198f88dc153e80041ab32fb20f6294e1440a77d Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Tue, 9 Jun 2026 09:57:09 +0200 +Subject: [PATCH] authServices: Use alternative to Promise.withResolvers() + +It doesn't exist in this version +--- + js/gdm/authServices.js | 6 +++++- + 1 file changed, 5 insertions(+), 1 deletion(-) + +diff --git a/js/gdm/authServices.js b/js/gdm/authServices.js +index 410f0bc16..fbea348fe 100644 +--- a/js/gdm/authServices.js ++++ b/js/gdm/authServices.js +@@ -266,7 +266,11 @@ export class AuthServices extends GObject.Object { + return GLib.SOURCE_REMOVE; + }); + +- const {promise, resolve, reject} = Promise.withResolvers(); ++ let resolve, reject; ++ const promise = new Promise((res, rej) => { ++ resolve = res; ++ reject = rej; ++ }); + const task = Gio.Task.new(this, cancellable, () => { + try { + const res = task.propagate_boolean(); +-- +2.54.0 + diff --git a/gnome-shell.spec b/gnome-shell.spec index aba5791..8475e39 100644 --- a/gnome-shell.spec +++ b/gnome-shell.spec @@ -42,6 +42,7 @@ Patch: 0001-loginManager-Update-RegisterSession.patch # Passwordless work # https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/3212 Patch: 0001-Support-for-web-login-and-unified-auth-mechanism.patch +Patch: 0002-post-changes-for-passwordless-gdm-backport.patch # Some users might have a broken PAM config, so we really need this # downstream patch to stop trying on configuration errors. Patch: 0001-gdm-Work-around-failing-fingerprint-auth.patch