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 3c391a2..273c3a1 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 14645c77a9f2df2ed822df73ef29bb610535c7a4 Mon Sep 17 00:00:00 2001 +From 3c1861b42dfcefafa240bae0270702c7d9c9a2fd Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Thu, 2 Oct 2025 10:59:57 +0200 -Subject: [PATCH 01/30] style: Add common login dialog button styles to avoid +Subject: [PATCH 01/42] style: Add common login dialog button styles to avoid duplication This will be used in next commits, when new login buttons are added. @@ -83,58 +83,85 @@ index b661e93c8d..6cca1e28e9 100644 2.53.0 -From d17d0aa4ee0e4d334ca4b0e5cda595065ca275f3 Mon Sep 17 00:00:00 2001 -From: Ray Strode -Date: Sun, 24 Nov 2024 12:53:50 -0500 -Subject: [PATCH 02/30] unlockDialog: Add some small fixes +From bfcebf4a5a60c64e5cbec78dbb8b646b69b571da 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 + height -1. Vertically center dialog if clock is active, otherwise reduce vertical - margins with a fixed top. -2. The unlock dialog currently fails to reuse the username, when requested - to do so. This fixes that. -3. Wait until authPrompt is destroyed (clock transition animation is complete) - to switch VT. +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 | 24 ++++++++++++++++-------- - 1 file changed, 16 insertions(+), 8 deletions(-) + js/ui/unlockDialog.js | 20 +++++++++++++++----- + 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/js/ui/unlockDialog.js b/js/ui/unlockDialog.js -index 63ba591eec..03d91b6114 100644 +index 63ba591eec..5d14cb9555 100644 --- a/js/ui/unlockDialog.js +++ b/js/ui/unlockDialog.js -@@ -449,8 +449,9 @@ class UnlockDialogLayout extends Clutter.LayoutManager { +@@ -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 NotificationsBox = GObject.registerClass({ + Signals: {'wake-up-screen': {}}, + }, class NotificationsBox extends St.BoxLayout { +@@ -449,8 +451,8 @@ class UnlockDialogLayout extends Clutter.LayoutManager { vfunc_allocate(container, box) { let [width, height] = box.get_size(); - let tenthOfHeight = height / 10.0; - let thirdOfHeight = height / 3.0; + const tenthOfHeight = height / 10.0; -+ const quarterOfHeight = height / 4.0; + const centerY = height / 2.0; let [, , stackWidth, stackHeight] = this._stack.get_preferred_size(); -@@ -476,9 +477,16 @@ class UnlockDialogLayout extends Clutter.LayoutManager { +@@ -476,9 +478,17 @@ 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 (this._activePage === this._clock) { ++ if (dialog._activePage === dialog._clock) { + stackY = Math.min( + Math.floor(centerY - stackHeight / 2.0), + height - stackHeight - maxNotificationsHeight); + } else { + stackY = Math.min( -+ quarterOfHeight, ++ Math.floor(centerY - FIXED_PROMPT_HEIGHT / 2.0), + height - stackHeight - maxNotificationsHeight); + } actorBox.x1 = columnX1; actorBox.y1 = stackY; -@@ -838,7 +846,7 @@ export const UnlockDialog = GObject.registerClass({ +-- +2.53.0 + + +From 6873ad575042d38fb5ab0663728694110db60c4a 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 + +The condition was checking for PROVIDE_USERNAME specifically, but +should also handle REUSE_USERNAME. Check for not DONT_PROVIDE_USERNAME +instead to correctly reuse the username when requested. +--- + 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 5d14cb9555..b6d71cd47f 100644 +--- a/js/ui/unlockDialog.js ++++ b/js/ui/unlockDialog.js +@@ -848,7 +848,7 @@ export const UnlockDialog = GObject.registerClass({ _onReset(authPrompt, beginRequest) { let userName; @@ -143,7 +170,28 @@ index 63ba591eec..03d91b6114 100644 this._authPrompt.setUser(this._user); userName = this._userName; } else { -@@ -888,8 +896,8 @@ export const UnlockDialog = GObject.registerClass({ +-- +2.53.0 + + +From c3467f84952ae46add711482d121969c9c55633d 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 + 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(-) + +diff --git a/js/ui/unlockDialog.js b/js/ui/unlockDialog.js +index b6d71cd47f..02c302ce7a 100644 +--- a/js/ui/unlockDialog.js ++++ b/js/ui/unlockDialog.js +@@ -898,8 +898,8 @@ export const UnlockDialog = GObject.registerClass({ } _otherUserClicked() { @@ -158,10 +206,183 @@ index 63ba591eec..03d91b6114 100644 2.53.0 -From fd68f33f22b525817e77fd1673845f3362c8c2ef Mon Sep 17 00:00:00 2001 +From 0c642b409fab1374dd466a64b450d4a67e151452 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 + updateSensitivity + +Replace the boolean parameter with a destructured object to make +call sites self-documenting. +--- + js/gdm/authPrompt.js | 18 +++++++++--------- + js/gdm/loginDialog.js | 6 +++--- + js/ui/unlockDialog.js | 2 +- + 3 files changed, 13 insertions(+), 13 deletions(-) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index 3b4a2f7988..526dcdbaa0 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -287,7 +287,7 @@ export const AuthPrompt = GObject.registerClass({ + + _activateNext(shouldSpin) { + this.verificationStatus = AuthPromptStatus.VERIFICATION_IN_PROGRESS; +- this.updateSensitivity(false); ++ this.updateSensitivity({sensitive: false}); + + if (this._queryingService) { + if (shouldSpin) +@@ -350,7 +350,7 @@ export const AuthPrompt = GObject.registerClass({ + else + this.setQuestion(question.replace(/[::] *$/, '').trim()); + +- this.updateSensitivity(true); ++ this.updateSensitivity({sensitive: true}); + this.emit('prompted'); + } + +@@ -364,7 +364,7 @@ export const AuthPrompt = GObject.registerClass({ + this._preemptiveAnswer = null; + + this.setChoiceList(promptMessage, choiceList); +- this.updateSensitivity(true); ++ this.updateSensitivity({sensitive: true}); + this.emit('prompted'); + } + +@@ -419,7 +419,7 @@ export const AuthPrompt = GObject.registerClass({ + this.clear(); + } + +- this.updateSensitivity(canRetry); ++ this.updateSensitivity({sensitive: canRetry}); + this.setActorInDefaultButtonWell(null); + + if (!canRetry) +@@ -540,12 +540,12 @@ export const AuthPrompt = GObject.registerClass({ + opacity: 0, + visible: true, + }); +- this.updateSensitivity(false); ++ this.updateSensitivity({sensitive: false}); + this._authList.ease({ + opacity: 255, + duration: MESSAGE_FADE_OUT_ANIMATION_TIME, + transition: Clutter.AnimationMode.EASE_OUT_QUAD, +- onComplete: () => this.updateSensitivity(true), ++ onComplete: () => this.updateSensitivity({sensitive: true}), + }); + } + +@@ -611,7 +611,7 @@ export const AuthPrompt = GObject.registerClass({ + wiggle(this._message, wiggleParameters); + } + +- updateSensitivity(sensitive) { ++ updateSensitivity({sensitive}) { + let authWidget; + + if (this._authList.visible) +@@ -641,7 +641,7 @@ export const AuthPrompt = GObject.registerClass({ + + this.setUser(null); + +- this.updateSensitivity(true); ++ this.updateSensitivity({sensitive: true}); + this._entry.set_text(''); + } + +@@ -734,7 +734,7 @@ export const AuthPrompt = GObject.registerClass({ + hold: null, + }); + +- this.updateSensitivity(false); ++ this.updateSensitivity({sensitive: false}); + + let hold = params.hold; + if (!hold) +diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js +index 5aca896db9..9f76464ad5 100644 +--- a/js/gdm/loginDialog.js ++++ b/js/gdm/loginDialog.js +@@ -1148,8 +1148,8 @@ export const LoginDialog = GObject.registerClass({ + () => { + this._authPrompt.disconnect(this._nextSignalId); + this._nextSignalId = 0; +- this._authPrompt.updateSensitivity(false); +- let answer = this._authPrompt.getAnswer(); ++ this._authPrompt.updateSensitivity({sensitive: false}); ++ const answer = this._authPrompt.getAnswer(); + this._user = this._userManager.get_user(answer); + this._authPrompt.clear(); + this._authPrompt.begin({userName: answer}); +@@ -1158,7 +1158,7 @@ export const LoginDialog = GObject.registerClass({ + this._updateCancelButton(); + + this._sessionMenuButton.updateSensitivity(false); +- this._authPrompt.updateSensitivity(true); ++ this._authPrompt.updateSensitivity({sensitive: true}); + this._showPrompt(); + } + +diff --git a/js/ui/unlockDialog.js b/js/ui/unlockDialog.js +index 02c302ce7a..7f4082b92a 100644 +--- a/js/ui/unlockDialog.js ++++ b/js/ui/unlockDialog.js +@@ -765,7 +765,7 @@ export const UnlockDialog = GObject.registerClass({ + case AuthPromptStatus.VERIFICATION_FAILED: + this._authPrompt.reset(); + this._authPrompt.updateSensitivity( +- verificationStatus === AuthPromptStatus.NOT_VERIFYING); ++ {sensitive: verificationStatus === AuthPromptStatus.NOT_VERIFYING}); + } + } + +-- +2.53.0 + + +From f30150c922a3aa4fd97101f9a2b0362ba84a029a 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 + updateSensitivity + +Replace the if/else widget selection with array-based lookup to +prepare for additional auth widgets in upcoming commits. +--- + 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 526dcdbaa0..7967b60a81 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -612,12 +612,9 @@ export const AuthPrompt = GObject.registerClass({ + } + + updateSensitivity({sensitive}) { +- let authWidget; +- +- if (this._authList.visible) +- authWidget = this._authList; +- else +- authWidget = this._entry; ++ const authWidget = [ ++ this._authList, ++ ].find(widget => widget.visible) ?? this._entry; + + if (authWidget.reactive === sensitive) + return; +-- +2.53.0 + + +From 356968d5a09cca83d0cc7ac53774fd42092ae042 Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Fri, 9 Feb 2024 09:02:25 -0500 -Subject: [PATCH 03/30] authPrompt: Fade out input buttons/entry after +Subject: [PATCH 07/42] authPrompt: Fade out input buttons/entry after verification It's nice to just see the user image and post login messages @@ -177,7 +398,7 @@ authList. It'll be used in next commits. 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index 3b4a2f7988..46ae96fa43 100644 +index 7967b60a81..33d46feba8 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js @@ -51,6 +51,7 @@ export const AuthPrompt = GObject.registerClass({ @@ -229,7 +450,7 @@ index 3b4a2f7988..46ae96fa43 100644 opacity: 0, visible: true, }); - this.updateSensitivity(false); + this.updateSensitivity({sensitive: false}); - this._authList.ease({ + element.ease({ opacity: 255, @@ -245,7 +466,7 @@ index 3b4a2f7988..46ae96fa43 100644 getAnswer() { diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js -index 5aca896db9..f3839aae76 100644 +index 9f76464ad5..f75497065c 100644 --- a/js/gdm/loginDialog.js +++ b/js/gdm/loginDialog.js @@ -590,6 +590,7 @@ export const LoginDialog = GObject.registerClass({ @@ -277,10 +498,10 @@ index 5aca896db9..f3839aae76 100644 2.53.0 -From 2e93995f199a23f1e5586c865c17ed5a0f418c1f Mon Sep 17 00:00:00 2001 +From 34e3c9dc1d829860eccb5a75e34b6d0c9d6c35dc Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Wed, 21 Jan 2026 14:23:34 +0100 -Subject: [PATCH 04/30] authPrompt: Don't reset preemptiveAnswer when +Subject: [PATCH 08/42] authPrompt: Don't reset preemptiveAnswer when VERIFICATION_IN_PROGRESS PreemptiveAnswer wasn't being used in the case where verification is in @@ -293,10 +514,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 46ae96fa43..d2df52783f 100644 +index 33d46feba8..ec3c2ec546 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -688,7 +688,8 @@ export const AuthPrompt = GObject.registerClass({ +@@ -685,7 +685,8 @@ export const AuthPrompt = GObject.registerClass({ this.verificationStatus = AuthPromptStatus.NOT_VERIFYING; this.cancelButton.reactive = this._hasCancelButton; this.cancelButton.can_focus = this._hasCancelButton; @@ -310,46 +531,16 @@ index 46ae96fa43..d2df52783f 100644 2.53.0 -From 1c3df3584af15dddb8392644da84c8ab2d57a685 Mon Sep 17 00:00:00 2001 +From fdd96fe191abfaea4fd29242fd7df8e78200a17e Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez -Date: Sun, 24 Nov 2024 16:29:43 -0500 -Subject: [PATCH 05/30] authPrompt: Add some changes to the elements in - _mainBox +Date: Thu, 12 Feb 2026 17:05:32 +0100 +Subject: [PATCH 09/42] style: Increase hint-text left margin - * Update the prompt entry style to be a bit bigger and more rounded. - - * Position cancelButton anchored to left side of password entry. - To do this, make _mainBox a St.Widget with a BinLayout. Then add - a constraint to cancelButton pivot_point. - - * Have a next button (cevron icon), embedded in the password entry. - Use a container (_entryArea) which contains _entry and - _defaultButtonWell (has _nextButton and _spinner), the latter is - overlaying _entry aligned on right. - - * _entryArea fades in when showing. - Use _fadeInElement (this sets sensitivity to true). - Now by default it's not visible. Don't clear it on verificationFailed to - keep it visible, it'll be cleared later on reset. - Since it is hidden by default, we must explicitly make it visible when - showing a message if no other widgets are visible. This allows for - a preemptive answer. - - * Fix animation when updating next button or spinning. - Now _deafaultButtonWell can have either _nextButton or _spinner, - so just use startSpinning and stopSpinning to set one or the oter. - Emit a 'loading' signal when starting/stopping spinning (used in - next commits). - Also, refactor setActorInDefaultButtonWell to make it simpler. - - * Increase left margin of hint-text - The cursor was overlapping the hint-text. - This allows proper readability of hint-text. +The cursor was overlapping the hint-text, making it difficult to read. +Increase the left margin to ensure proper readability. --- - .../gnome-shell-sass/widgets/_entries.scss | 2 +- - .../gnome-shell-sass/widgets/_login-lock.scss | 24 ++- - js/gdm/authPrompt.js | 182 ++++++++++-------- - 3 files changed, 126 insertions(+), 82 deletions(-) + data/theme/gnome-shell-sass/widgets/_entries.scss | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/theme/gnome-shell-sass/widgets/_entries.scss b/data/theme/gnome-shell-sass/widgets/_entries.scss index 41e10f7663..5a45e86d6d 100644 @@ -363,6 +554,79 @@ index 41e10f7663..5a45e86d6d 100644 + margin-left: $base_margin * 2; } } +-- +2.53.0 + + +From 2532bcfc85315b588f4c5c6ddaee7a3287fa8911 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 + +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(-) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index ec3c2ec546..5a62e8897f 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -80,14 +80,16 @@ export const AuthPrompt = GObject.registerClass({ + + this._userVerifier = this._createUserVerifier(this._gdmClient, {reauthenticationOnly}); + +- this._userVerifier.connect('ask-question', this._onAskQuestion.bind(this)); +- this._userVerifier.connect('show-message', this._onShowMessage.bind(this)); +- this._userVerifier.connect('show-choice-list', this._onShowChoiceList.bind(this)); +- this._userVerifier.connect('verification-failed', this._onVerificationFailed.bind(this)); +- this._userVerifier.connect('verification-complete', this._onVerificationComplete.bind(this)); +- this._userVerifier.connect('reset', this._onReset.bind(this)); +- this._userVerifier.connect('smartcard-status-changed', this._onSmartcardStatusChanged.bind(this)); +- this._userVerifier.connect('credential-manager-authenticated', this._onCredentialManagerAuthenticated.bind(this)); ++ this._userVerifier.connectObject( ++ 'ask-question', this._onAskQuestion.bind(this), ++ 'show-message', this._onShowMessage.bind(this), ++ 'show-choice-list', this._onShowChoiceList.bind(this), ++ 'verification-failed', this._onVerificationFailed.bind(this), ++ 'verification-complete', this._onVerificationComplete.bind(this), ++ 'reset', this._onReset.bind(this), ++ 'smartcard-status-changed', this._onSmartcardStatusChanged.bind(this), ++ 'credential-manager-authenticated', this._onCredentialManagerAuthenticated.bind(this), ++ this); + this.smartcardDetected = this._userVerifier.smartcardDetected; + + this.connect('destroy', this._onDestroy.bind(this)); +@@ -141,6 +143,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 + + +From f634f1e7ff47de5a3a042b0da4d7af6b06f6831f 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 + +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. +--- + .../gnome-shell-sass/widgets/_login-lock.scss | 24 ++++++- + js/gdm/authPrompt.js | 68 ++++++++++++++----- + 2 files changed, 72 insertions(+), 20 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 --- a/data/theme/gnome-shell-sass/widgets/_login-lock.scss @@ -418,7 +682,7 @@ index 6cca1e28e9..93dbe617b7 100644 .conflicting-session-dialog-content { diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index d2df52783f..175a04eb28 100644 +index 5a62e8897f..474b6ecd9c 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js @@ -2,6 +2,7 @@ import Clutter from 'gi://Clutter'; @@ -429,23 +693,7 @@ index d2df52783f..175a04eb28 100644 import Pango from 'gi://Pango'; import Shell from 'gi://Shell'; import St from 'gi://St'; -@@ -16,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; -@@ -52,6 +52,7 @@ export const AuthPrompt = GObject.registerClass({ - 'prompted': {}, - 'reset': {param_types: [GObject.TYPE_UINT]}, - 'verification-complete': {}, -+ 'loading': {param_types: [GObject.TYPE_BOOLEAN]}, - }, - }, class AuthPrompt extends St.BoxLayout { - _init(gdmClient, mode) { -@@ -155,9 +156,11 @@ export const AuthPrompt = GObject.registerClass({ +@@ -158,9 +159,11 @@ export const AuthPrompt = GObject.registerClass({ } _initInputRow() { @@ -459,7 +707,7 @@ index d2df52783f..175a04eb28 100644 }); this.add_child(this._mainBox); -@@ -167,10 +170,17 @@ export const AuthPrompt = GObject.registerClass({ +@@ -170,10 +173,17 @@ export const AuthPrompt = GObject.registerClass({ button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE, reactive: this._hasCancelButton, can_focus: this._hasCancelButton, @@ -477,7 +725,7 @@ index d2df52783f..175a04eb28 100644 if (this._hasCancelButton) this.cancelButton.connect('clicked', () => this.cancel()); else -@@ -198,10 +208,20 @@ export const AuthPrompt = GObject.registerClass({ +@@ -201,10 +211,20 @@ export const AuthPrompt = GObject.registerClass({ }); this._mainBox.add_child(this._authList); @@ -499,7 +747,7 @@ index d2df52783f..175a04eb28 100644 }; this._entry = null; -@@ -213,8 +233,7 @@ export const AuthPrompt = GObject.registerClass({ +@@ -216,8 +236,7 @@ export const AuthPrompt = GObject.registerClass({ ShellEntry.addContextMenu(this._passwordEntry, {actionMode: Shell.ActionMode.NONE}); this._entry = this._passwordEntry; @@ -509,17 +757,7 @@ index d2df52783f..175a04eb28 100644 this._inactiveEntry = this._textEntry; this._timedLoginIndicator = new St.Bin({ -@@ -230,26 +249,33 @@ export const AuthPrompt = GObject.registerClass({ - this._fadeOutMessage(); - }); - -- entry.clutter_text.connect('activate', () => { -- let shouldSpin = entry === this._passwordEntry; -- if (entry.reactive) -- this._activateNext(shouldSpin); -- }); -+ entry.clutter_text.connect('activate', () => this._activateNext()); - }); +@@ -242,17 +261,28 @@ export const AuthPrompt = GObject.registerClass({ this._defaultButtonWell = new St.Widget({ layout_manager: new Clutter.BinLayout(), @@ -553,25 +791,7 @@ index d2df52783f..175a04eb28 100644 } showTimedLoginIndicator(time) { -@@ -286,12 +312,15 @@ export const AuthPrompt = GObject.registerClass({ - this._timedLoginIndicator.scale_x = 0.; - } - -- _activateNext(shouldSpin) { -+ _activateNext() { -+ if (!this._entry.reactive) -+ return; -+ - this.verificationStatus = AuthPromptStatus.VERIFICATION_IN_PROGRESS; - this.updateSensitivity(false); - - if (this._queryingService) { -- if (shouldSpin) -+ if (this._entry === this._passwordEntry) - this.startSpinning(); - - this._userVerifier.answerQuery(this._queryingService, this._entry.text); -@@ -319,7 +348,7 @@ export const AuthPrompt = GObject.registerClass({ +@@ -322,7 +352,7 @@ export const AuthPrompt = GObject.registerClass({ } if (newEntry) { @@ -580,15 +800,143 @@ index d2df52783f..175a04eb28 100644 this._entry = newEntry; this._inactiveEntry = inactiveEntry; -@@ -351,7 +380,6 @@ export const AuthPrompt = GObject.registerClass({ +@@ -424,17 +454,17 @@ export const AuthPrompt = GObject.registerClass({ + } + + this.updateSensitivity({sensitive: canRetry}); +- this.setActorInDefaultButtonWell(null); ++ this.setActorInDefaultButtonWell(this._nextButton); + + if (!canRetry) + this.verificationStatus = AuthPromptStatus.VERIFICATION_FAILED; + + if (wasQueryingService) +- wiggle(this._entry); ++ wiggle(this._entryArea); + } + + _onVerificationComplete() { +- this.setActorInDefaultButtonWell(null); ++ this.setActorInDefaultButtonWell(this._nextButton, true); + this.verificationStatus = AuthPromptStatus.VERIFICATION_SUCCEEDED; + + this._mainBox.reactive = false; +@@ -547,7 +577,8 @@ export const AuthPrompt = GObject.registerClass({ + this._entry.hint_text = question; + + this._authList.hide(); +- this._entry.show(); ++ ++ this._entryArea.show(); + this._entry.grab_key_focus(); + } + +@@ -573,7 +604,7 @@ export const AuthPrompt = GObject.registerClass({ + this._authList.addItem(key, text); + } + +- this._entry.hide(); ++ this._entryArea.hide(); + if (this._message.text === '') + this._message.hide(); + this._fadeInElement(this._authList); +@@ -635,6 +666,9 @@ export const AuthPrompt = GObject.registerClass({ + if (authWidget.reactive === sensitive) + return; + ++ if (authWidget === this._entry) ++ this._nextButton.reactive = sensitive; ++ + authWidget.reactive = sensitive; + + if (sensitive) { +@@ -648,7 +682,7 @@ export const AuthPrompt = GObject.registerClass({ + } + + vfunc_hide() { +- this.setActorInDefaultButtonWell(null, true); ++ this.setActorInDefaultButtonWell(this._nextButton, true); + super.vfunc_hide(); + this._message.opacity = 0; + +-- +2.53.0 + + +From e44919241a35ba1a0e515cf8b05f181dd0506688 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 + +This means when clear it's hidden. Keep it visible on verification failed, +it'll be cleared later on reset. +--- + 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 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -384,7 +384,6 @@ export const AuthPrompt = GObject.registerClass({ else this.setQuestion(question.replace(/[::] *$/, '').trim()); -- this.updateSensitivity(true); +- this.updateSensitivity({sensitive: true}); this.emit('prompted'); } -@@ -409,29 +437,35 @@ export const AuthPrompt = GObject.registerClass({ +@@ -448,10 +447,8 @@ export const AuthPrompt = GObject.registerClass({ + _onVerificationFailed(userVerifier, serviceName, canRetry) { + const wasQueryingService = this._queryingService === serviceName; + +- if (wasQueryingService) { ++ if (wasQueryingService) + this._queryingService = null; +- this.clear(); +- } + + this.updateSensitivity({sensitive: canRetry}); + this.setActorInDefaultButtonWell(this._nextButton); +@@ -557,6 +554,7 @@ export const AuthPrompt = GObject.registerClass({ + } + + clear() { ++ this._entryArea.hide(); + this._entry.text = ''; + this._inactiveEntry.text = ''; + this.stopSpinning(); +@@ -578,8 +576,7 @@ export const AuthPrompt = GObject.registerClass({ + + this._authList.hide(); + +- this._entryArea.show(); +- this._entry.grab_key_focus(); ++ this._fadeInElement(this._entryArea); + } + + _fadeInElement(element) { +-- +2.53.0 + + +From 67209dda9cfc1be9a87b2765406a5a8e7bd11af8 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 + +Since entryArea is hidden by default, we must explicitly make it +visible when showing a message if no other widgets are visible. +This allows getting a preemptive answer. +--- + js/gdm/authPrompt.js | 8 ++++++++ + 1 file changed, 8 insertions(+) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index d639d574e4..d876a3acf5 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -441,6 +441,14 @@ export const AuthPrompt = GObject.registerClass({ } this.setMessage(message, type, wiggleParameters); @@ -603,34 +951,98 @@ index d2df52783f..175a04eb28 100644 this.emit('prompted'); } - _onVerificationFailed(userVerifier, serviceName, canRetry) { - const wasQueryingService = this._queryingService === serviceName; +-- +2.53.0 + + +From ae7d20481b14b922d45c20e3d02585ca0526e390 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 + +- 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. +--- + js/gdm/authPrompt.js | 105 +++++++++++++++++++------------------------ + 1 file changed, 46 insertions(+), 59 deletions(-) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index d876a3acf5..1f78b70bd4 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'; -- if (wasQueryingService) { -+ if (wasQueryingService) + 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({ + 'prompted': {}, + 'reset': {param_types: [GObject.TYPE_UINT]}, + 'verification-complete': {}, ++ 'loading': {param_types: [GObject.TYPE_BOOLEAN]}, + }, + }, class AuthPrompt extends St.BoxLayout { + _init(gdmClient, mode) { +@@ -252,11 +252,7 @@ export const AuthPrompt = GObject.registerClass({ + this._fadeOutMessage(); + }); + +- entry.clutter_text.connect('activate', () => { +- let shouldSpin = entry === this._passwordEntry; +- if (entry.reactive) +- this._activateNext(shouldSpin); +- }); ++ entry.clutter_text.connect('activate', () => this._activateNext()); + }); + + this._defaultButtonWell = new St.Widget({ +@@ -319,12 +315,15 @@ export const AuthPrompt = GObject.registerClass({ + this._timedLoginIndicator.scale_x = 0.; + } + +- _activateNext(shouldSpin) { ++ _activateNext() { ++ if (!this._entry.reactive) ++ return; ++ + this.verificationStatus = AuthPromptStatus.VERIFICATION_IN_PROGRESS; + this.updateSensitivity({sensitive: false}); + + if (this._queryingService) { +- if (shouldSpin) ++ if (this._entry === this._passwordEntry) + this.startSpinning(); + + this._userVerifier.answerQuery(this._queryingService, this._entry.text); +@@ -459,7 +458,7 @@ export const AuthPrompt = GObject.registerClass({ this._queryingService = null; -- this.clear(); -- } - this.updateSensitivity(canRetry); -- this.setActorInDefaultButtonWell(null); + this.updateSensitivity({sensitive: canRetry}); +- this.setActorInDefaultButtonWell(this._nextButton); + this.stopSpinning(); if (!canRetry) this.verificationStatus = AuthPromptStatus.VERIFICATION_FAILED; - - if (wasQueryingService) -- wiggle(this._entry); -+ wiggle(this._entryArea); +@@ -469,7 +468,7 @@ export const AuthPrompt = GObject.registerClass({ } _onVerificationComplete() { -- this.setActorInDefaultButtonWell(null); +- this.setActorInDefaultButtonWell(this._nextButton, true); + this.stopSpinning(true); this.verificationStatus = AuthPromptStatus.VERIFICATION_SUCCEEDED; this._mainBox.reactive = false; -@@ -451,79 +485,68 @@ export const AuthPrompt = GObject.registerClass({ +@@ -489,76 +488,64 @@ export const AuthPrompt = GObject.registerClass({ } setActorInDefaultButtonWell(actor, animate) { @@ -743,45 +1155,11 @@ index d2df52783f..175a04eb28 100644 } clear() { -+ this._entryArea.hide(); - this._entry.text = ''; - this._inactiveEntry.text = ''; - this.stopSpinning(); -@@ -544,8 +567,8 @@ export const AuthPrompt = GObject.registerClass({ - this._entry.hint_text = question; - - this._authList.hide(); -- this._entry.show(); -- this._entry.grab_key_focus(); -+ -+ this._fadeInElement(this._entryArea); - } - - _fadeInElement(element) { -@@ -570,7 +593,7 @@ export const AuthPrompt = GObject.registerClass({ - this._authList.addItem(key, text); - } - -- this._entry.hide(); -+ this._entryArea.hide(); - if (this._message.text === '') - this._message.hide(); - this._fadeInElement(this._authList); -@@ -635,6 +658,9 @@ export const AuthPrompt = GObject.registerClass({ - if (authWidget.reactive === sensitive) - return; - -+ if (authWidget === this._entry) -+ this._nextButton.reactive = sensitive; -+ - authWidget.reactive = sensitive; - - if (sensitive) { -@@ -648,7 +674,7 @@ export const AuthPrompt = GObject.registerClass({ +@@ -687,7 +674,7 @@ export const AuthPrompt = GObject.registerClass({ } vfunc_hide() { -- this.setActorInDefaultButtonWell(null, true); +- this.setActorInDefaultButtonWell(this._nextButton, true); + this.stopSpinning(); super.vfunc_hide(); this._message.opacity = 0; @@ -790,10 +1168,10 @@ index d2df52783f..175a04eb28 100644 2.53.0 -From 2229de4921be07ecae638f97a04df58753e29999 Mon Sep 17 00:00:00 2001 +From b502b7c6e06a77c17ba225e91539f60118e39328 Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Tue, 6 Feb 2024 13:06:32 -0500 -Subject: [PATCH 06/30] authPrompt: Parameterize reset function +Subject: [PATCH 15/42] authPrompt: Parameterize reset function In the future, userVerifier will request a partial reset where some state is carried over or explicitly specified. @@ -801,14 +1179,14 @@ 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 | 63 ++++++++++++++++++++++++++++---------------- - 1 file changed, 40 insertions(+), 23 deletions(-) + js/gdm/authPrompt.js | 35 +++++++++++++++++++++++++---------- + 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index 175a04eb28..602f2a1d00 100644 +index 1f78b70bd4..2614b82410 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -479,9 +479,11 @@ export const AuthPrompt = GObject.registerClass({ +@@ -482,9 +482,11 @@ export const AuthPrompt = GObject.registerClass({ this.emit('verification-complete'); } @@ -823,15 +1201,35 @@ index 175a04eb28..602f2a1d00 100644 } setActorInDefaultButtonWell(actor, animate) { -@@ -709,14 +711,23 @@ export const AuthPrompt = GObject.registerClass({ +@@ -548,10 +550,17 @@ export const AuthPrompt = GObject.registerClass({ + this.setActorInDefaultButtonWell(this._nextButton, animate); + } + +- clear() { ++ clear(params) { ++ const {reuseEntryText} = Params.parse(params, { ++ reuseEntryText: false, ++ }); ++ ++ if (!reuseEntryText) { ++ this._entry.text = ''; ++ this._inactiveEntry.text = ''; ++ } ++ + 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({ this.updateSensitivity(false); } - reset() { - let oldStatus = this.verificationStatus; + reset(params) { -+ let {beginRequestType, reuseEntryText, softReset} = Params.parse(params, { -+ beginRequestType: null, ++ const {reuseEntryText, softReset} = Params.parse(params, { + reuseEntryText: false, + softReset: false, + }); @@ -840,77 +1238,171 @@ index 175a04eb28..602f2a1d00 100644 this.verificationStatus = AuthPromptStatus.NOT_VERIFYING; this.cancelButton.reactive = this._hasCancelButton; this.cancelButton.can_focus = this._hasCancelButton; - if (oldStatus !== AuthPromptStatus.VERIFICATION_IN_PROGRESS) - this._preemptiveAnswer = null; +@@ -726,7 +740,7 @@ export const AuthPrompt = GObject.registerClass({ + this._userVerifier.cancel(); -+ const oldEntryText = this._textEntry.text; -+ const oldPasswordText = this._passwordEntry.text; -+ - if (this._preemptiveAnswerWatchId) - this._idleMonitor.remove_watch(this._preemptiveAnswerWatchId); - this._preemptiveAnswerWatchId = this._idleMonitor.add_idle_watch(3000, -@@ -732,29 +743,35 @@ export const AuthPrompt = GObject.registerClass({ + this._queryingService = null; +- this.clear(); ++ this.clear({reuseEntryText}); + this._message.opacity = 0; + this.setUser(null); this._updateEntry(true); - this.stopSpinning(); - -+ if (reuseEntryText) { -+ this._textEntry.text = oldEntryText; -+ this._passwordEntry.text = oldPasswordText; -+ } -+ - if (oldStatus === AuthPromptStatus.VERIFICATION_FAILED) - this.emit('failed'); - else if (oldStatus === AuthPromptStatus.VERIFICATION_CANCELLED) - this.emit('cancelled'); - -- let beginRequestType; -- -- if (this._mode === AuthPromptMode.UNLOCK_ONLY) { -- // The user is constant at the unlock screen, so it will immediately -- // respond to the request with the username -- if (oldStatus === AuthPromptStatus.VERIFICATION_CANCELLED) -- return; -- beginRequestType = BeginRequestType.PROVIDE_USERNAME; -- } else if (this._userVerifier.foregroundServiceDeterminesUsername()) { -- // 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; +@@ -749,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; - } else if (oldStatus === AuthPromptStatus.VERIFICATION_IN_PROGRESS) { -- // We're going back to retry with current user -- beginRequestType = BeginRequestType.REUSE_USERNAME; -- } else { -- // In all other cases, we should get the username up front. -- beginRequestType = BeginRequestType.PROVIDE_USERNAME; -+ if (beginRequestType === null) { -+ if (this._mode === AuthPromptMode.UNLOCK_ONLY) { -+ // The user is constant at the unlock screen, so it will immediately -+ // respond to the request with the username -+ if (oldStatus === AuthPromptStatus.VERIFICATION_CANCELLED) -+ return; -+ beginRequestType = BeginRequestType.PROVIDE_USERNAME; -+ } else if (!this._userVerifier.needsUsername()) { -+ // 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; -+ } else if (oldStatus === AuthPromptStatus.VERIFICATION_IN_PROGRESS || -+ softReset) { -+ // We're going back to retry with current user -+ beginRequestType = BeginRequestType.REUSE_USERNAME; -+ } else { -+ // In all other cases, we should get the username up front. -+ beginRequestType = BeginRequestType.PROVIDE_USERNAME; -+ } - } - - this.emit('reset', beginRequestType); ++ } else if (oldStatus === AuthPromptStatus.VERIFICATION_IN_PROGRESS || ++ softReset) { + // We're going back to retry with current user + beginRequestType = BeginRequestType.REUSE_USERNAME; + } else { -- 2.53.0 -From 22b6abee7ee54589bc10281006fcfd6ea0315a68 Mon Sep 17 00:00:00 2001 +From 090803f771f3c1b800ac256c0b91b50f07c12813 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 + +The enum is emitted with the 'reset' signal and describes the type +of reset being performed, so ResetType is a clearer name. +--- + js/gdm/authPrompt.js | 14 +++++++------- + js/gdm/loginDialog.js | 6 +++--- + js/ui/unlockDialog.js | 4 ++-- + 3 files changed, 12 insertions(+), 12 deletions(-) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index 2614b82410..ea09f470b4 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -38,7 +38,7 @@ export const AuthPromptStatus = { + }; + + /** @enum {number} */ +-export const BeginRequestType = { ++export const ResetType = { + PROVIDE_USERNAME: 0, + DONT_PROVIDE_USERNAME: 1, + REUSE_USERNAME: 2, +@@ -751,28 +751,28 @@ export const AuthPrompt = GObject.registerClass({ + else if (oldStatus === AuthPromptStatus.VERIFICATION_CANCELLED) + this.emit('cancelled'); + +- let beginRequestType; ++ let resetType; + + if (this._mode === AuthPromptMode.UNLOCK_ONLY) { + // The user is constant at the unlock screen, so it will immediately + // respond to the request with the username + if (oldStatus === AuthPromptStatus.VERIFICATION_CANCELLED) + return; +- beginRequestType = BeginRequestType.PROVIDE_USERNAME; ++ resetType = ResetType.PROVIDE_USERNAME; + } else if (this._userVerifier.foregroundServiceDeterminesUsername()) { + // 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; ++ resetType = ResetType.DONT_PROVIDE_USERNAME; + } else if (oldStatus === AuthPromptStatus.VERIFICATION_IN_PROGRESS || + softReset) { + // We're going back to retry with current user +- beginRequestType = BeginRequestType.REUSE_USERNAME; ++ resetType = ResetType.REUSE_USERNAME; + } else { + // In all other cases, we should get the username up front. +- beginRequestType = BeginRequestType.PROVIDE_USERNAME; ++ resetType = ResetType.PROVIDE_USERNAME; + } + +- this.emit('reset', beginRequestType); ++ this.emit('reset', resetType); + } + + addCharacter(unichar) { +diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js +index f75497065c..f38de0c914 100644 +--- a/js/gdm/loginDialog.js ++++ b/js/gdm/loginDialog.js +@@ -1066,7 +1066,7 @@ export const LoginDialog = GObject.registerClass({ + } + } + +- _onReset(authPrompt, beginRequest) { ++ _onReset(authPrompt, resetType) { + this._ensureGreeterProxy(); + this._sessionMenuButton.updateSensitivity(true); + +@@ -1078,11 +1078,11 @@ export const LoginDialog = GObject.registerClass({ + this._nextSignalId = 0; + } + +- if (previousUser && beginRequest === AuthPrompt.BeginRequestType.REUSE_USERNAME) { ++ if (previousUser && resetType === AuthPrompt.ResetType.REUSE_USERNAME) { + this._user = previousUser; + this._authPrompt.setUser(this._user); + this._authPrompt.begin({userName: previousUser.get_user_name()}); +- } else if (beginRequest === AuthPrompt.BeginRequestType.PROVIDE_USERNAME) { ++ } else if (resetType === AuthPrompt.ResetType.PROVIDE_USERNAME) { + if (!this._disableUserList) + this._showUserList(); + else +diff --git a/js/ui/unlockDialog.js b/js/ui/unlockDialog.js +index 7f4082b92a..c52abc4d7e 100644 +--- a/js/ui/unlockDialog.js ++++ b/js/ui/unlockDialog.js +@@ -846,9 +846,9 @@ export const UnlockDialog = GObject.registerClass({ + this.emit('failed'); + } + +- _onReset(authPrompt, beginRequest) { ++ _onReset(authPrompt, resetType) { + let userName; +- if (beginRequest !== AuthPrompt.BeginRequestType.DONT_PROVIDE_USERNAME) { ++ if (resetType !== AuthPrompt.ResetType.DONT_PROVIDE_USERNAME) { + this._authPrompt.setUser(this._user); + userName = this._userName; + } else { +-- +2.53.0 + + +From f0da5c72c6ee44eee36a572aa8f97ce48f742480 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 + preemptive input + +isgraph returns true for printable characters except space, while +isprint includes space as a valid character. This allows users to +type passwords containing spaces during preemptive input. +--- + 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 +--- a/js/ui/unlockDialog.js ++++ b/js/ui/unlockDialog.js +@@ -686,7 +686,7 @@ export const UnlockDialog = GObject.registerClass({ + + this._showPrompt(); + +- if (GLib.unichar_isgraph(unichar)) ++ if (GLib.unichar_isprint(unichar)) + this._authPrompt.addCharacter(unichar); + + return Clutter.EVENT_PROPAGATE; +-- +2.53.0 + + +From 30b8c4d9febced8814d37c9f55835950a438d2ac Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Wed, 21 Jan 2026 14:34:11 +0100 -Subject: [PATCH 07/30] authPrompt: Capture preemptive input before entry is +Subject: [PATCH 18/42] authPrompt: Capture preemptive input before entry is sensitive The previous changes made the entry invisible and insensitive until @@ -920,25 +1412,29 @@ 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. + +Also, remove unsused addCharacter() in loginDialog.js. --- - js/gdm/authPrompt.js | 36 ++++++++++++++++++++++++++++-------- + js/gdm/authPrompt.js | 39 +++++++++++++++++++++++++++++++-------- + js/gdm/loginDialog.js | 4 ---- js/ui/unlockDialog.js | 2 +- - 2 files changed, 29 insertions(+), 9 deletions(-) + 3 files changed, 32 insertions(+), 13 deletions(-) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index 602f2a1d00..a073ab21a8 100644 +index ea09f470b4..5a09c3063b 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -152,6 +152,18 @@ export const AuthPrompt = GObject.registerClass({ +@@ -155,6 +155,19 @@ export const AuthPrompt = GObject.registerClass({ this.cancel(); return Clutter.EVENT_STOP; } + + if (this._preemptiveInput && !this._pendingActivate) { + const unichar = event.get_key_unicode(); -+ if (event.get_key_symbol() === Clutter.KEY_Return) { ++ if (event.get_key_symbol() === Clutter.KEY_Return && ++ this._entry.clutter_text.text) { + this._pendingActivate = true; -+ } else if (GLib.unichar_isgraph(unichar)) { ++ } else if (GLib.unichar_isprint(unichar)) { + this._entry.clutter_text.insert_text(unichar, + this._entry.clutter_text.cursor_position); + } @@ -948,19 +1444,19 @@ index 602f2a1d00..a073ab21a8 100644 return Clutter.EVENT_PROPAGATE; } -@@ -583,10 +595,20 @@ export const AuthPrompt = GObject.registerClass({ +@@ -593,10 +606,20 @@ export const AuthPrompt = GObject.registerClass({ opacity: 255, duration: MESSAGE_FADE_OUT_ANIMATION_TIME, transition: Clutter.AnimationMode.EASE_OUT_QUAD, -- onComplete: () => this.updateSensitivity(true), +- onComplete: () => this.updateSensitivity({sensitive: true}), + onComplete: () => { -+ this.updateSensitivity(true); -+ this._endPreemptiveInput(element); ++ this.updateSensitivity({sensitive: true}); ++ this._completePreemptiveInput(element); + }, }); } -+ _endPreemptiveInput(element) { ++ _completePreemptiveInput(element) { + if (element === this._entryArea && this._pendingActivate) + this._activateNext(); + this._preemptiveInput = false; @@ -970,17 +1466,26 @@ index 602f2a1d00..a073ab21a8 100644 setChoiceList(promptMessage, choiceList) { this._authList.clear(); this._authList.label.text = promptMessage; -@@ -743,7 +765,7 @@ export const AuthPrompt = GObject.registerClass({ - this._updateEntry(true); - this.stopSpinning(); +@@ -719,7 +742,7 @@ export const AuthPrompt = GObject.registerClass({ + } -- if (reuseEntryText) { -+ if (reuseEntryText || this._preemptiveInput) { - this._textEntry.text = oldEntryText; - this._passwordEntry.text = oldPasswordText; - } -@@ -777,12 +799,10 @@ export const AuthPrompt = GObject.registerClass({ - this.emit('reset', beginRequestType); + reset(params) { +- const {reuseEntryText, softReset} = Params.parse(params, { ++ let {reuseEntryText, softReset} = Params.parse(params, { + reuseEntryText: false, + softReset: false, + }); +@@ -739,6 +762,8 @@ export const AuthPrompt = GObject.registerClass({ + if (this._userVerifier) + this._userVerifier.cancel(); + ++ reuseEntryText = reuseEntryText || this._preemptiveInput; ++ + this._queryingService = null; + this.clear({reuseEntryText}); + this._message.opacity = 0; +@@ -775,12 +800,10 @@ export const AuthPrompt = GObject.registerClass({ + this.emit('reset', resetType); } - addCharacter(unichar) { @@ -996,14 +1501,29 @@ index 602f2a1d00..a073ab21a8 100644 } begin(params) { +diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js +index f38de0c914..09e8073c65 100644 +--- a/js/gdm/loginDialog.js ++++ b/js/gdm/loginDialog.js +@@ -1593,10 +1593,6 @@ export const LoginDialog = GObject.registerClass({ + this._authPrompt.cancel(); + } + +- addCharacter(_unichar) { +- // Don't allow type ahead at the login screen +- } +- + finish(onComplete) { + this._authPrompt.finish(onComplete); + } diff --git a/js/ui/unlockDialog.js b/js/ui/unlockDialog.js -index 03d91b6114..a8bdb93b8c 100644 +index a648243a24..fffc58e07c 100644 --- a/js/ui/unlockDialog.js +++ b/js/ui/unlockDialog.js -@@ -685,7 +685,7 @@ export const UnlockDialog = GObject.registerClass({ +@@ -687,7 +687,7 @@ export const UnlockDialog = GObject.registerClass({ this._showPrompt(); - if (GLib.unichar_isgraph(unichar)) + if (GLib.unichar_isprint(unichar)) - this._authPrompt.addCharacter(unichar); + this._authPrompt.startPreemptiveInput(unichar); @@ -1013,10 +1533,10 @@ index 03d91b6114..a8bdb93b8c 100644 2.53.0 -From caffa2924f7e521c05d9eb3b26dba3388c6b333f Mon Sep 17 00:00:00 2001 +From 7a086c8fb3c08377c554e96202c0e183a5968646 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez -Date: Thu, 11 Sep 2025 19:12:05 +0200 -Subject: [PATCH 08/30] authPrompt: On verificationFailed ensure input +Date: Tue, 10 Feb 2026 18:51:41 +0100 +Subject: [PATCH 19/42] authPrompt: On verificationFailed ensure input sensitivity is disabled There's a time window between the verification failing and a new @@ -1030,15 +1550,15 @@ verification process, reenabling sensitivity. 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index a073ab21a8..5cba71c813 100644 +index 5a09c3063b..a383208ecc 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -466,7 +466,7 @@ export const AuthPrompt = GObject.registerClass({ +@@ -470,7 +470,7 @@ export const AuthPrompt = GObject.registerClass({ if (wasQueryingService) this._queryingService = null; -- this.updateSensitivity(canRetry); -+ this.updateSensitivity(false); +- this.updateSensitivity({sensitive: canRetry}); ++ this.updateSensitivity({sensitive: false}); this.stopSpinning(); if (!canRetry) @@ -1046,36 +1566,35 @@ index a073ab21a8..5cba71c813 100644 2.53.0 -From 9edee1f213696a661b441825149a50fa50d5efa8 Mon Sep 17 00:00:00 2001 +From f4faa89270778bd0280c911cd5524498444205ef Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Tue, 30 Sep 2025 17:43:22 +0200 -Subject: [PATCH 09/30] authPrompt: Update authList style +Subject: [PATCH 20/42] authPrompt: Update authList style -Make the buttons a bit bigger and more rounded. +Make the AuthListItem buttons a bit bigger and more rounded. -Buttons can have two labels and one icon. -The icon is used to inform about Organization field of the certificate when -hovering over it. This will mainly be used by the next commit, for the -moment keep relying only in one single label (update choiceList list -format). +AuthListItem now accepts a content object with title, subtitle, iconName, +iconTitle, and iconSubtitle properties. This allows displaying richer +item content. The icon button shows a popup with additional details. Now the title is an insensitive button that is in _mainBox to ensure back button is visible and properly aligned. -Add accessible_name. +Use accessible_name. + +Register classes with the new style. --- - .../gnome-shell-sass/widgets/_login-lock.scss | 91 ++++++++++-- - js/gdm/authList.js | 134 ++++++++++++++++-- - js/gdm/authPrompt.js | 28 +++- + .../gnome-shell-sass/widgets/_login-lock.scss | 95 ++++++++-- + js/gdm/authList.js | 168 +++++++++++++++--- + js/gdm/authPrompt.js | 28 ++- js/gdm/util.js | 6 +- - po/POTFILES.in | 1 + - 5 files changed, 227 insertions(+), 33 deletions(-) + 4 files changed, 249 insertions(+), 48 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..5dbc00a718 100644 +index 93dbe617b7..5a31725b1d 100644 --- a/data/theme/gnome-shell-sass/widgets/_login-lock.scss +++ b/data/theme/gnome-shell-sass/widgets/_login-lock.scss -@@ -141,42 +141,103 @@ $_gdm_dialog_width: 25em; +@@ -141,42 +141,107 @@ $_gdm_dialog_width: 25em; // Authentication methods list .login-dialog-auth-list-view { -st-vfade-offset: 3em; @@ -1145,25 +1664,25 @@ index 93dbe617b7..5dbc00a718 100644 - &:rtl { padding-right: $base_padding * 2.5; text-align: right; } + padding: $base_padding; + text-align: center; - } - -+.login-dialog-auth-list-item-first-line, -+.login-dialog-auth-list-item-second-line { ++} ++ ++.login-dialog-auth-list-item-title, ++.login-dialog-auth-list-item-subtitle { + @extend %heading; + text-align: center; + padding: $base_padding * 0.3 0; +} + -+.login-dialog-auth-list-item-first-line { ++.login-dialog-auth-list-item-title { + color: $_gdm_fg; -+} -+ -+.login-dialog-auth-list-item-second-line { + } + ++.login-dialog-auth-list-item-subtitle { + color: darken($_gdm_fg, 20%); + font-weight: 500; +} + -+.login-dialog-auth-list-item-icon { ++.login-dialog-item-icon { + width: 1.3em; + height: 1.3em; + color: $_gdm_fg; @@ -1173,21 +1692,25 @@ index 93dbe617b7..5dbc00a718 100644 + &:hover { + background-color: $system_bg_color; + } -+} + -+.login-dialog-auth-list-item-popup-box { -+ .login-dialog-auth-list-item-popup-labels { -+ spacing: $base_padding * 0.5; -+ @extend %heading; -+ text-align: center; -+ -+ & > :first-child { -+ color: darken($fg_color, 30%); -+ font-weight: 500; ++ &-popup { ++ &.popup-menu { ++ min-width: 0; + } + -+ & > :last-child { -+ color: $fg_color; ++ &-box &-labels { ++ spacing: $base_padding * 0.5; ++ @extend %heading; ++ text-align: center; ++ ++ & > :first-child { ++ color: darken($fg_color, 30%); ++ font-weight: 500; ++ } ++ ++ & > :last-child { ++ color: $fg_color; ++ } + } + } +} @@ -1195,10 +1718,10 @@ index 93dbe617b7..5dbc00a718 100644 // User list .login-dialog-user-list-view { diff --git a/js/gdm/authList.js b/js/gdm/authList.js -index 4873a05c58..674cb5dc9f 100644 +index 4873a05c58..3ce30efc29 100644 --- a/js/gdm/authList.js +++ b/js/gdm/authList.js -@@ -18,29 +18,65 @@ +@@ -18,30 +18,141 @@ import Clutter from 'gi://Clutter'; import GLib from 'gi://GLib'; import GObject from 'gi://GObject'; @@ -1212,149 +1735,173 @@ index 4873a05c58..674cb5dc9f 100644 + const SCROLL_ANIMATION_TIME = 500; -+const PopupLabel = class extends PopupMenu.PopupMenu { -+ constructor(sourceActor, label) { -+ super(sourceActor, 0.5, St.Side.TOP); -+ -+ const menuItem = new PopupMenu.PopupBaseMenuItem({ -+ reactive: false, -+ can_focus: false, -+ }); -+ menuItem.add_child(label); -+ this.addMenuItem(menuItem); -+ -+ Main.uiGroup.add_child(this.actor); -+ -+ // Overwrite min-width to allow smaller width than the default -+ this.actor.set_style('min-width: 0;'); -+ } -+}; -+ - const AuthListItem = GObject.registerClass({ - Signals: {'activate': {}}, - }, class AuthListItem extends St.Button { +-const AuthListItem = GObject.registerClass({ +- Signals: {'activate': {}}, +-}, class AuthListItem extends St.Button { - _init(key, text) { -+ _init(key, content) { - this.key = key; +- this.key = key; - const label = new St.Label({ - text, - style_class: 'login-dialog-auth-list-label', +- y_align: Clutter.ActorAlign.CENTER, +- x_expand: true, ++const ItemIconPopup = class extends PopupMenu.PopupMenu { ++ constructor(sourceActor, title, subtitle) { ++ super(sourceActor, 0.5, St.Side.TOP); + -+ this._container = new St.Widget({ -+ layout_manager: new Clutter.BinLayout(), -+ x_expand: true, -+ }); -+ this._labelBox = new St.BoxLayout({ ++ this.box.add_style_class_name('login-dialog-item-icon-popup-box'); ++ this.actor.add_style_class_name('login-dialog-item-icon-popup'); ++ ++ const labels = new St.BoxLayout({ + orientation: Clutter.Orientation.VERTICAL, - y_align: Clutter.ActorAlign.CENTER, - x_expand: true, - }); -+ this._container.add_child(this._labelBox); ++ style_class: 'login-dialog-item-icon-popup-labels', ++ }); ++ labels.add_child(new St.Label({text: title})); ++ labels.add_child(new St.Label({text: subtitle})); + -+ const {commonName, description, organization} = content; -+ this._appendLine(commonName); -+ this._appendLine(description); -+ this._appendIcon(organization); ++ const item = new PopupMenu.PopupBaseMenuItem({ ++ reactive: false, ++ can_focus: false, ++ }); ++ item.add_child(labels); ++ this.addMenuItem(item); ++ ++ sourceActor.connect('clicked', () => this.toggle()); ++ sourceActor.connect('destroy', () => this.destroy()); ++ ++ this.actor.hide(); ++ ++ 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); ++ } ++ ++ constructor(iconName, iconTitle, iconSubtitle) { ++ super({ ++ style_class: 'login-dialog-item-icon', ++ child: new St.Icon({icon_name: iconName}), ++ }); ++ ++ this._popup = new ItemIconPopup(this, iconTitle, iconSubtitle); ++ Main.uiGroup.add_child(this._popup.actor); ++ } ++} ++ ++class AuthListItem extends St.Button { ++ static [GObject.GTypeName] = 'AuthListItem'; ++ ++ static [GObject.signals] = { ++ 'activate': {}, ++ }; ++ ++ static { ++ GObject.registerClass(this); ++ } - super._init({ +- super._init({ ++ constructor(key, content) { ++ const {title, subtitle, iconName, iconTitle, iconSubtitle} = content; ++ ++ super({ style_class: 'login-dialog-auth-list-item', button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE, can_focus: true, - child: label, -+ child: this._container, reactive: true, -+ accessible_name: [commonName, organization, description] ++ accessible_name: [title, subtitle, iconTitle, iconSubtitle] + .filter(p => p) -+ .join(' '), ++ .join(', '), ++ }); ++ ++ this.key = key; ++ ++ this._container = new St.Widget({ ++ layout_manager: new Clutter.BinLayout(), ++ x_expand: true, }); ++ this._labelBox = new St.BoxLayout({ ++ orientation: Clutter.Orientation.VERTICAL, ++ y_align: Clutter.ActorAlign.CENTER, ++ x_expand: true, ++ }); ++ this._container.add_child(this._labelBox); ++ ++ if (title) { ++ const label = new St.Label({ ++ text: title, ++ style_class: 'login-dialog-auth-list-item-title', ++ y_align: Clutter.ActorAlign.CENTER, ++ x_expand: true, ++ }); ++ this._labelBox.add_child(label); ++ } ++ ++ if (subtitle) { ++ const label = new St.Label({ ++ text: subtitle, ++ style_class: 'login-dialog-auth-list-item-subtitle', ++ y_align: Clutter.ActorAlign.CENTER, ++ x_expand: true, ++ }); ++ this._labelBox.add_child(label); ++ } ++ ++ if (iconName && iconTitle && iconSubtitle) { ++ const icon = new ItemIcon(iconName, iconTitle, iconSubtitle); ++ icon.add_constraint(new Clutter.AlignConstraint({ ++ source: this._container, ++ align_axis: Clutter.AlignAxis.X_AXIS, ++ factor: 0.5, ++ })); ++ icon.add_constraint(new Clutter.AlignConstraint({ ++ source: this._container, ++ align_axis: Clutter.AlignAxis.Y_AXIS, ++ factor: 0.0, ++ pivot_point: new Graphene.Point({x: 0.0, y: 0.35}), ++ })); ++ this._container.add_child(icon); ++ } ++ ++ this.set_child(this._container); this.connect('key-focus-in', -@@ -53,6 +89,83 @@ const AuthListItem = GObject.registerClass({ - this.connect('clicked', this._onClicked.bind(this)); + () => this._setSelected(true)); +@@ -65,25 +176,29 @@ const AuthListItem = GObject.registerClass({ + this.remove_style_pseudo_class('selected'); + } } +-}); ++} -+ _appendLine(text) { -+ if (!text) -+ return; +-export const AuthList = GObject.registerClass({ +- Signals: { ++export class AuthList extends St.BoxLayout { ++ static [GObject.GTypeName] = 'AuthList'; + -+ if (!this._firstLine) { -+ const label = new St.Label({ -+ text, -+ style_class: 'login-dialog-auth-list-item-first-line', -+ y_align: Clutter.ActorAlign.CENTER, -+ x_expand: true, -+ }); -+ this._labelBox.add_child(label); -+ this._firstLine = label; -+ } else if (!this._secondLine) { -+ const label = new St.Label({ -+ text, -+ style_class: 'login-dialog-auth-list-item-second-line', -+ y_align: Clutter.ActorAlign.CENTER, -+ x_expand: true, -+ }); -+ this._labelBox.add_child(label); -+ this._secondLine = label; -+ } ++ static [GObject.signals] = { + 'activate': {param_types: [GObject.TYPE_STRING]}, + 'item-added': {param_types: [AuthListItem.$gtype]}, +- }, +-}, class AuthList extends St.BoxLayout { +- _init() { +- super._init({ ++ }; ++ ++ static { ++ GObject.registerClass(this); + } + -+ _appendIcon(text) { -+ if (!text || this._icon) -+ return; -+ -+ const icon = new St.Button({ -+ style_class: 'login-dialog-auth-list-item-icon', -+ child: new St.Icon({icon_name: 'vcard-symbolic'}), -+ }); -+ icon.add_constraint(new Clutter.AlignConstraint({ -+ source: this._container, -+ align_axis: Clutter.AlignAxis.X_AXIS, -+ factor: 0.5, -+ })); -+ icon.add_constraint(new Clutter.AlignConstraint({ -+ source: this._container, -+ align_axis: Clutter.AlignAxis.Y_AXIS, -+ factor: 0.0, -+ pivot_point: new Graphene.Point({x: 0.0, y: 0.35}), -+ })); -+ -+ const textLines = [_('Organization'), text]; -+ this.popupLabel = this._createPopupLabel(icon, textLines); -+ -+ this._container.add_child(icon); -+ this._icon = icon; -+ } -+ -+ _createPopupLabel(sourceActor, textLines) { -+ const labelsContainer = new St.BoxLayout({ -+ orientation: Clutter.Orientation.VERTICAL, -+ style_class: 'login-dialog-auth-list-item-popup-labels', -+ }); -+ textLines.forEach(text => { -+ labelsContainer.add_child(new St.Label({text})); -+ }); -+ -+ const popup = new PopupLabel(sourceActor, labelsContainer); -+ popup.box.add_style_class_name('login-dialog-auth-list-item-popup-box'); -+ popup.actor.hide(); -+ -+ if (!this._menuManager) { -+ this._menuManager = new PopupMenu.PopupMenuManager(sourceActor, { -+ actionMode: Shell.ActionMode.NONE, -+ }); -+ } -+ this._menuManager.addMenu(popup); -+ -+ sourceActor.connect('clicked', () => popup.toggle()); -+ -+ return popup; -+ } -+ - _onClicked() { - this.emit('activate'); - } -@@ -77,13 +190,11 @@ export const AuthList = GObject.registerClass({ - super._init({ ++ constructor() { ++ super({ orientation: Clutter.Orientation.VERTICAL, style_class: 'login-dialog-auth-list-layout', - x_align: Clutter.ActorAlign.START, @@ -1369,7 +1916,7 @@ index 4873a05c58..674cb5dc9f 100644 this._box = new St.BoxLayout({ orientation: Clutter.Orientation.VERTICAL, style_class: 'login-dialog-auth-list', -@@ -136,10 +247,10 @@ export const AuthList = GObject.registerClass({ +@@ -136,10 +251,10 @@ export const AuthList = GObject.registerClass({ }); } @@ -1382,7 +1929,7 @@ index 4873a05c58..674cb5dc9f 100644 this._box.add_child(item); this._items.set(key, item); -@@ -170,7 +281,6 @@ export const AuthList = GObject.registerClass({ +@@ -170,8 +285,7 @@ export const AuthList = GObject.registerClass({ } clear() { @@ -1390,11 +1937,13 @@ index 4873a05c58..674cb5dc9f 100644 this._box.destroy_all_children(); this._items.clear(); } +-}); ++} diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index 5cba71c813..4be4c4f7cd 100644 +index a383208ecc..8d7dfb9bae 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -212,13 +212,30 @@ export const AuthPrompt = GObject.registerClass({ +@@ -216,13 +216,30 @@ export const AuthPrompt = GObject.registerClass({ duration: MESSAGE_FADE_OUT_ANIMATION_TIME, mode: Clutter.AnimationMode.EASE_OUT_QUAD, onComplete: () => { @@ -1426,15 +1975,15 @@ index 5cba71c813..4be4c4f7cd 100644 this._entryArea = new St.Widget({ style_class: 'login-dialog-prompt-entry-area', -@@ -564,6 +581,7 @@ export const AuthPrompt = GObject.registerClass({ - this._entry.text = ''; - this._inactiveEntry.text = ''; +@@ -575,6 +592,7 @@ export const AuthPrompt = GObject.registerClass({ + + this._entryArea.hide(); this.stopSpinning(); + this._authListTitle.child.text = ''; this._authList.clear(); this._authList.hide(); -@@ -611,10 +629,10 @@ export const AuthPrompt = GObject.registerClass({ +@@ -622,10 +640,10 @@ export const AuthPrompt = GObject.registerClass({ setChoiceList(promptMessage, choiceList) { this._authList.clear(); @@ -1450,42 +1999,30 @@ index 5cba71c813..4be4c4f7cd 100644 this._entryArea.hide(); diff --git a/js/gdm/util.js b/js/gdm/util.js -index b027d16e8d..fdd89b4e10 100644 +index 4b0f763f0a..b71251458e 100644 --- a/js/gdm/util.js +++ b/js/gdm/util.js -@@ -728,7 +728,11 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -723,7 +723,11 @@ export class ShellUserVerifier extends Signals.EventEmitter { if (!this.serviceIsForeground(serviceName)) return; - this.emit('show-choice-list', serviceName, promptMessage, list.deepUnpack()); -+ const choiceList = Object.fromEntries( -+ Object.entries(list.deepUnpack()) -+ .map(([key, value]) => [key, {description: value}])); ++ const choiceList = {}; ++ for (const [key, value] of Object.entries(list.deepUnpack())) ++ choiceList[key] = {title: value}; + + this.emit('show-choice-list', serviceName, promptMessage, choiceList); } _onInfo(client, serviceName, info) { -diff --git a/po/POTFILES.in b/po/POTFILES.in -index ee0829c96e..8be425615d 100644 ---- a/po/POTFILES.in -+++ b/po/POTFILES.in -@@ -10,6 +10,7 @@ data/X-GNOME-Shell-System.directory.desktop.in - data/X-GNOME-Shell-Utilities.directory.desktop.in - js/dbusServices/extensions/extensionPrefsDialog.js - js/dbusServices/extensions/ui/extension-error-page.ui -+js/gdm/authList.js - js/gdm/authPrompt.js - js/gdm/loginDialog.js - js/gdm/util.js -- 2.53.0 -From c410ba6f5aded2f1327eea0a98048ecf2e50fdb1 Mon Sep 17 00:00:00 2001 +From 199fec44007815189b8b23fad04b199a40982768 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Wed, 8 Oct 2025 18:48:07 +0200 -Subject: [PATCH 10/30] authPrompt: Let back button go back to step 1 instead +Subject: [PATCH 21/42] 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 @@ -1499,7 +2036,7 @@ Show backButton in unlockDialog for these cases. 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index 4be4c4f7cd..73d5bae28d 100644 +index 8d7dfb9bae..3326096a2d 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js @@ -70,6 +70,7 @@ export const AuthPrompt = GObject.registerClass({ @@ -1510,7 +2047,7 @@ index 4be4c4f7cd..73d5bae28d 100644 this._idleMonitor = global.backend.get_core_idle_monitor(); -@@ -99,8 +100,6 @@ export const AuthPrompt = GObject.registerClass({ +@@ -101,8 +102,6 @@ export const AuthPrompt = GObject.registerClass({ }); this.add_child(this._userWell); @@ -1519,7 +2056,7 @@ index 4be4c4f7cd..73d5bae28d 100644 this._initInputRow(); let capsLockPlaceholder = new St.Label(); -@@ -180,8 +179,8 @@ export const AuthPrompt = GObject.registerClass({ +@@ -184,8 +183,8 @@ export const AuthPrompt = GObject.registerClass({ style_class: 'login-dialog-button cancel-button', accessible_name: _('Cancel'), button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE, @@ -1530,7 +2067,7 @@ index 4be4c4f7cd..73d5bae28d 100644 x_expand: true, x_align: Clutter.ActorAlign.START, y_align: Clutter.ActorAlign.CENTER, -@@ -193,10 +192,8 @@ export const AuthPrompt = GObject.registerClass({ +@@ -197,10 +196,8 @@ export const AuthPrompt = GObject.registerClass({ pivot_point: new Graphene.Point({x: 1, y: 0}), })); @@ -1543,7 +2080,7 @@ index 4be4c4f7cd..73d5bae28d 100644 this._mainBox.add_child(this.cancelButton); this._authList = new AuthList.AuthList(); -@@ -307,6 +304,16 @@ export const AuthPrompt = GObject.registerClass({ +@@ -311,6 +308,16 @@ export const AuthPrompt = GObject.registerClass({ this.setActorInDefaultButtonWell(this._nextButton); } @@ -1560,7 +2097,7 @@ index 4be4c4f7cd..73d5bae28d 100644 showTimedLoginIndicator(time) { let hold = new Batch.Hold(); -@@ -393,6 +400,9 @@ export const AuthPrompt = GObject.registerClass({ +@@ -397,6 +404,9 @@ export const AuthPrompt = GObject.registerClass({ this.clear(); this._queryingService = serviceName; @@ -1570,7 +2107,7 @@ index 4be4c4f7cd..73d5bae28d 100644 if (this._preemptiveAnswer) { this._userVerifier.answerQuery(this._queryingService, this._preemptiveAnswer); this._preemptiveAnswer = null; -@@ -417,6 +427,8 @@ export const AuthPrompt = GObject.registerClass({ +@@ -421,6 +431,8 @@ export const AuthPrompt = GObject.registerClass({ this.clear(); this._queryingService = serviceName; @@ -1579,7 +2116,7 @@ index 4be4c4f7cd..73d5bae28d 100644 if (this._preemptiveAnswer) this._preemptiveAnswer = null; -@@ -760,10 +772,10 @@ export const AuthPrompt = GObject.registerClass({ +@@ -767,10 +779,10 @@ export const AuthPrompt = GObject.registerClass({ const oldStatus = this.verificationStatus; this.verificationStatus = AuthPromptStatus.NOT_VERIFYING; @@ -1590,9 +2127,9 @@ index 4be4c4f7cd..73d5bae28d 100644 + this._promptStep = 0; + this._updateCancelButton(); - const oldEntryText = this._textEntry.text; - const oldPasswordText = this._passwordEntry.text; -@@ -857,6 +869,12 @@ export const AuthPrompt = GObject.registerClass({ + if (this._preemptiveAnswerWatchId) + this._idleMonitor.remove_watch(this._preemptiveAnswerWatchId); +@@ -858,6 +870,12 @@ export const AuthPrompt = GObject.registerClass({ if (this.verificationStatus === AuthPromptStatus.VERIFICATION_SUCCEEDED) return; @@ -1609,20 +2146,32 @@ index 4be4c4f7cd..73d5bae28d 100644 2.53.0 -From 45fa26714c486b00cddfcb251ab2f99024323239 Mon Sep 17 00:00:00 2001 +From 09d762ac05a793f5df5fd51be2e674fd0929252d Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Mon, 20 Oct 2025 17:24:40 +0200 -Subject: [PATCH 11/30] loginDialog: Vertically align promptAuth with fixed top +Subject: [PATCH 22/42] loginDialog: Vertically center authPrompt using fixed + height +Use a fixed estimated height for centering so the position stays +stable regardless of actual content height changes during user +interaction. --- - js/gdm/loginDialog.js | 21 ++++++++++++++++++++- - 1 file changed, 20 insertions(+), 1 deletion(-) + js/gdm/loginDialog.js | 25 ++++++++++++++++++++++++- + 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js -index f3839aae76..d9194cb9fe 100644 +index 09e8073c65..3a22bf161b 100644 --- a/js/gdm/loginDialog.js +++ b/js/gdm/loginDialog.js -@@ -732,6 +732,25 @@ export const LoginDialog = GObject.registerClass({ +@@ -45,6 +45,7 @@ import * as A11y from '../ui/status/accessibility.js'; + + const _FADE_ANIMATION_TIME = 250; + const _SCROLL_ANIMATION_TIME = 500; ++const _FIXED_TOP_ACTOR_HEIGHT = 400; + const _TIMED_LOGIN_IDLE_THRESHOLD = 5.0; + const _CONFLICTING_SESSION_DIALOG_TIMEOUT = 60; + +@@ -732,6 +733,28 @@ export const LoginDialog = GObject.registerClass({ return actorBox; } @@ -1630,12 +2179,15 @@ index f3839aae76..d9194cb9fe 100644 + const actorBox = new Clutter.ActorBox(); + + let [, , natWidth, natHeight] = actor.get_preferred_size(); -+ const centerX = dialogBox.x1 + (dialogBox.x2 - dialogBox.x1) / 2; -+ const marginTop = 0.22; -+ const top = dialogBox.y1 + (dialogBox.y2 - dialogBox.y1) * marginTop; ++ const dialogWidth = dialogBox.x2 - dialogBox.x1; ++ const dialogHeight = dialogBox.y2 - dialogBox.y1; ++ const centerX = dialogBox.x1 + dialogWidth / 2; ++ const centerY = dialogBox.y1 + dialogHeight / 2; + -+ natWidth = Math.min(natWidth, dialogBox.x2 - dialogBox.x1); -+ natHeight = Math.min(natHeight, dialogBox.y2 - dialogBox.y1); ++ const top = centerY - _FIXED_TOP_ACTOR_HEIGHT / 2; ++ ++ natWidth = Math.min(natWidth, dialogWidth); ++ natHeight = Math.min(natHeight, dialogHeight); + + actorBox.x1 = Math.floor(centerX - natWidth / 2); + actorBox.y1 = Math.floor(top); @@ -1648,7 +2200,7 @@ index f3839aae76..d9194cb9fe 100644 _getCenterActorAllocation(dialogBox, actor) { let actorBox = new Clutter.ActorBox(); -@@ -770,7 +789,7 @@ export const LoginDialog = GObject.registerClass({ +@@ -770,7 +793,7 @@ export const LoginDialog = GObject.registerClass({ let authPromptAllocation = null; let authPromptWidth = 0; if (this._authPrompt.visible) { @@ -1661,10 +2213,10 @@ index f3839aae76..d9194cb9fe 100644 2.53.0 -From 24ce2bcb4353f4a81528263332c5b7cbdd95579b Mon Sep 17 00:00:00 2001 +From cfec9c4e34f698ad5693d5e81daf1d31a8972ccb Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Tue, 12 Nov 2024 14:26:30 -0500 -Subject: [PATCH 12/30] data: Add fingerprint and vcard icons +Subject: [PATCH 23/42] data: Add fingerprint and vcard icons Fingerprint icon will be used to inform when it's being run un the background. @@ -1752,10 +2304,10 @@ index 0000000000..1694f23645 2.53.0 -From 3c30881e4265128cfdd307d844b47da50cbb58fa Mon Sep 17 00:00:00 2001 +From 4df4492dfd42af8f9449249ad2f5778b63e9f4d7 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Mon, 18 Aug 2025 12:30:50 +0200 -Subject: [PATCH 13/30] gdm: Extract authentication service and role constants +Subject: [PATCH 24/42] gdm: Extract authentication service and role constants to const.js Create a dedicated const.js module to centralize GDM authentication-related @@ -1774,7 +2326,7 @@ constants. create mode 100644 js/gdm/const.js diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index 73d5bae28d..72a1c0a04c 100644 +index 3326096a2d..7b3aa8558f 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js @@ -10,6 +10,7 @@ import St from 'gi://St'; @@ -1785,7 +2337,7 @@ index 73d5bae28d..72a1c0a04c 100644 import * as GdmUtil from './util.js'; import * as Params from '../misc/params.js'; import * as ShellEntry from '../ui/shellEntry.js'; -@@ -453,7 +454,7 @@ export const AuthPrompt = GObject.registerClass({ +@@ -457,7 +458,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. @@ -1811,7 +2363,7 @@ index 0000000000..2f37446c8a +export const FINGERPRINT_SERVICE_NAME = 'gdm-fingerprint'; +export const SWITCHABLE_AUTH_SERVICE_NAME = 'gdm-switchable-auth'; diff --git a/js/gdm/util.js b/js/gdm/util.js -index fdd89b4e10..cabd7cbf02 100644 +index b71251458e..660c6e4ddc 100644 --- a/js/gdm/util.js +++ b/js/gdm/util.js @@ -24,9 +24,6 @@ Gio._promisify(Gdm.UserVerifierProxy.prototype, @@ -1862,7 +2414,7 @@ index fdd89b4e10..cabd7cbf02 100644 export class ShellUserVerifier extends Signals.EventEmitter { constructor(client, params) { super(); -@@ -432,7 +460,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -431,7 +459,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { this._updateDefaultService(); if (this._userVerifier && @@ -1871,7 +2423,7 @@ index fdd89b4e10..cabd7cbf02 100644 if (!this._hold?.isAcquired()) this._hold = new Batch.Hold(); await this._maybeStartFingerprintVerification(); -@@ -486,8 +514,8 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -485,8 +513,8 @@ export class ShellUserVerifier extends Signals.EventEmitter { this.smartcardDetected = smartcardDetected; if (this.smartcardDetected) @@ -1882,7 +2434,7 @@ index fdd89b4e10..cabd7cbf02 100644 this._preemptingService = null; this._updateDefaultService(); -@@ -606,7 +634,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -605,7 +633,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { return true; } @@ -1891,7 +2443,7 @@ index fdd89b4e10..cabd7cbf02 100644 } serviceIsDefault(serviceName) { -@@ -615,7 +643,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -614,7 +642,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { serviceIsFingerprint(serviceName) { return this._fingerprintReaderType !== FingerprintReaderType.NONE && @@ -1900,7 +2452,7 @@ index fdd89b4e10..cabd7cbf02 100644 } _onSettingsChanged() { -@@ -632,7 +660,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -631,7 +659,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { this._fingerprintManager = null; this._fingerprintReaderType = FingerprintReaderType.NONE; @@ -1909,7 +2461,7 @@ index fdd89b4e10..cabd7cbf02 100644 needsReset = true; } -@@ -642,7 +670,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -641,7 +669,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { this._smartcardManager.disconnectObject(this); this._smartcardManager = null; @@ -1918,7 +2470,7 @@ index fdd89b4e10..cabd7cbf02 100644 needsReset = true; } -@@ -652,13 +680,13 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -651,13 +679,13 @@ export class ShellUserVerifier extends Signals.EventEmitter { _getDetectedDefaultService() { if (this._smartcardManager?.loggedInWithToken()) @@ -1936,7 +2488,7 @@ index fdd89b4e10..cabd7cbf02 100644 return null; } -@@ -668,7 +696,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -667,7 +695,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { if (!this._defaultService) { log('no authentication service is enabled, using password authentication'); @@ -1945,7 +2497,7 @@ index fdd89b4e10..cabd7cbf02 100644 } if (oldDefaultService && -@@ -720,8 +748,8 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -715,8 +743,8 @@ export class ShellUserVerifier extends Signals.EventEmitter { async _maybeStartFingerprintVerification() { if (this._userName && this._fingerprintReaderType !== FingerprintReaderType.NONE && @@ -1956,7 +2508,7 @@ index fdd89b4e10..cabd7cbf02 100644 } _onChoiceListQuery(client, serviceName, promptMessage, list) { -@@ -854,7 +882,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -848,7 +876,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { } async _verificationFailed(serviceName, shouldRetry) { @@ -1964,7 +2516,7 @@ index fdd89b4e10..cabd7cbf02 100644 + 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 @@ -1981,10 +2533,10 @@ index e5e6167f15..ee6b77d322 100644 2.53.0 -From 1e6e560c2e6eda3e82bd0a5e60e29b8d5f724708 Mon Sep 17 00:00:00 2001 +From 90848a72fd7a322d83a7f0319d31bf6a498e01d1 Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Tue, 6 Feb 2024 11:04:20 -0500 -Subject: [PATCH 14/30] gdm: Add new AuthMenuButton control +Subject: [PATCH 25/42] 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. @@ -2004,14 +2556,14 @@ 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 | 562 ++++++++++++++++++ + js/gdm/authMenuButton.js | 613 ++++++++++++++++++ js/js-resources.gresource.xml | 1 + js/ui/popupMenu.js | 22 + - 4 files changed, 627 insertions(+) + 4 files changed, 678 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 5dbc00a718..4b4f1a5d4e 100644 +index 5a31725b1d..45d47f0652 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; @@ -2065,10 +2617,10 @@ index 5dbc00a718..4b4f1a5d4e 100644 } diff --git a/js/gdm/authMenuButton.js b/js/gdm/authMenuButton.js new file mode 100644 -index 0000000000..cc45a8f44e +index 0000000000..b5018d8382 --- /dev/null +++ b/js/gdm/authMenuButton.js -@@ -0,0 +1,562 @@ +@@ -0,0 +1,613 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- +/* + * Copyright 2024 Red Hat, Inc @@ -2122,23 +2674,42 @@ index 0000000000..cc45a8f44e + +import * as BoxPointer from '../ui/boxpointer.js'; +import * as Main from '../ui/main.js'; -+import * as Params from '../misc/params.js'; +import * as PopupMenu from '../ui/popupMenu.js'; + -+const AuthMenuItem = GObject.registerClass( ++const VISIBILITY_ANIMATION_TIME = 200; ++ +class AuthMenuItem extends PopupMenu.PopupImageMenuItem { -+ _init(item, params) { -+ super._init(item.name, item.iconName || '', params); ++ static [GObject.GTypeName] = 'AuthMenuItem'; ++ ++ static { ++ GObject.registerClass(this); ++ } ++ ++ constructor(item, params) { ++ super(item.name, item.iconName || '', params); + + // Move ornament to the left + this.set_child_at_index(this._ornamentIcon, 0); + } -+}); + -+const AuthMenuItemIndicator = GObject.registerClass( ++ 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 { -+ _init(item, params) { -+ super._init(item, params); ++ static [GObject.GTypeName] = 'AuthMenuItemIndicator'; ++ ++ static { ++ GObject.registerClass(this); ++ } ++ ++ constructor(item, params) { ++ super(item, params); + + if (item.description) { + const box = new St.BoxLayout({ @@ -2156,47 +2727,64 @@ index 0000000000..cc45a8f44e + style_class: 'login-dialog-auth-menu-item-indicator-description', + }); + -+ const parent = this.label_actor.get_parent(); -+ + box.add_child(nameLabel); + box.add_child(descriptionLabel); + -+ parent.insert_child_below(box, this.label_actor); -+ -+ this.label_actor.destroy(); -+ this.label_actor = box; ++ this.updateLabelActor(box); + } + } -+}); ++} + -+export const AuthMenuButton = GObject.registerClass({ -+ Signals: {'active-item-changed': {param_types: [GObject.TYPE_STRING]}}, -+}, class AuthMenuButton extends St.Bin { -+ _init(params) { -+ params = Params.parse(params, { -+ title: '', -+ iconName: '', -+ readOnly: false, -+ sectionOrder: [], -+ }); ++export class AuthMenuButton extends St.Bin { ++ static [GObject.GTypeName] = 'AuthMenuButton'; + -+ this._readOnly = params.readOnly; ++ 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, ++ false), ++ 'section-order': GObject.ParamSpec.jsobject( ++ 'section-order', null, null, ++ GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY), ++ 'animate-visibility': GObject.ParamSpec.boolean( ++ 'animate-visibility', null, null, ++ GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY, ++ false), ++ }; ++ ++ static [GObject.signals] = { ++ 'active-item-changed': {param_types: [GObject.TYPE_STRING]}, ++ }; ++ ++ static { ++ GObject.registerClass(this); ++ } ++ ++ constructor(params) { ++ params.sectionOrder ??= []; ++ super(params); + + const button = new St.Button({ + style_class: 'login-dialog-button login-dialog-auth-menu-button', -+ child: new St.Icon({icon_name: params.iconName}), ++ child: new St.Icon({icon_name: this.iconName}), + reactive: true, + track_hover: true, + can_focus: true, -+ accessible_name: params.title, ++ accessible_name: this.title, + accessible_role: Atk.Role.MENU, + x_align: Clutter.ActorAlign.CENTER, + y_align: Clutter.ActorAlign.CENTER, + }); + -+ super._init({ -+ child: button, -+ }); ++ this.child = button; + + this._button = button; + @@ -2206,7 +2794,7 @@ index 0000000000..cc45a8f44e + this._menu.actor.hide(); + + this._menu.connect('open-state-changed', (_menu, isOpen) => { -+ if (this._readOnly) { ++ if (this.readOnly) { + if (isOpen) + this._addMenuShield(); + else @@ -2219,7 +2807,7 @@ index 0000000000..cc45a8f44e + this._manager.addMenu(this._menu); + + this._button.connect('clicked', () => { -+ if (this._readOnly && this._getVisibleItemsCount() === 1) ++ if (this.readOnly && this._getVisibleItemsCount() === 1) + return; + this._menu.toggle(); + }); @@ -2227,7 +2815,6 @@ index 0000000000..cc45a8f44e + this._items = new Map(); + this._activeItems = new Set(); + this._headers = new Map(); -+ this._sectionOrder = params.sectionOrder; + this.updateSensitivity(true); + } + @@ -2275,14 +2862,36 @@ index 0000000000..cc45a8f44e + this._sensitive = sensitive; + + const visibleItems = this._getVisibleItemsCount(); -+ if (visibleItems === 0 || (visibleItems <= 1 && !this._readOnly)) ++ if (visibleItems === 0 || (visibleItems <= 1 && !this.readOnly)) + sensitive = false; + + this._button.reactive = sensitive; + this._button.can_focus = sensitive; -+ this.opacity = sensitive ? 255 : 0; -+ this.visible = 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() { @@ -2306,8 +2915,8 @@ index 0000000000..cc45a8f44e + + const sections = Array.from(sectionsSet); + sections.sort((a, b) => { -+ const indexA = this._sectionOrder.indexOf(a); -+ const indexB = this._sectionOrder.indexOf(b); ++ const indexA = this.sectionOrder.indexOf(a); ++ const indexB = this.sectionOrder.indexOf(b); + + if (indexA !== -1 && indexB !== -1) + return indexA - indexB; @@ -2418,13 +3027,13 @@ index 0000000000..cc45a8f44e + }); + + let insertIndex = 0; -+ const orderIndex = this._sectionOrder.indexOf(sectionName); ++ 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); ++ const existingIndex = this.sectionOrder.indexOf(existingSectionName); + if (existingIndex === -1 || existingIndex > orderIndex) + break; + insertIndex++; @@ -2542,19 +3151,18 @@ index 0000000000..cc45a8f44e + close() { + this._menu.close(); + } -+}); ++} + -+export const AuthMenuButtonIndicator = GObject.registerClass( -+class AuthMenuButtonIndicator extends AuthMenuButton { -+ _init(params) { -+ params = Params.parse(params, { -+ title: '', -+ sectionOrder: [], -+ }); ++export class AuthMenuButtonIndicator extends AuthMenuButton { ++ static [GObject.GTypeName] = 'AuthMenuButtonIndicator'; + ++ static { ++ GObject.registerClass(this); ++ } ++ ++ constructor(params) { + params.readOnly = true; -+ -+ super._init(params); ++ super(params); + + this._button.add_style_class_name('login-dialog-auth-menu-button-indicator'); + @@ -2574,8 +3182,12 @@ index 0000000000..cc45a8f44e + + this._descriptionLabel = new St.Label({ + y_align: Clutter.ActorAlign.CENTER, -+ visible: false, + }); ++ this._descriptionLabel.bind_property_full('text', ++ this._descriptionLabel, 'visible', ++ GObject.BindingFlags.SYNC_CREATE, ++ (bind, source) => [true, !!source], ++ null); + container.add_child(this._descriptionLabel); + + this.child = container; @@ -2614,23 +3226,14 @@ index 0000000000..cc45a8f44e + } + + updateDescriptionLabel() { -+ if (this._items.size === 1) { -+ const item = this.getItems()[0]; -+ if (item?.description) { -+ this._descriptionLabel.text = item.description; -+ this._descriptionLabel.visible = true; -+ } else { -+ this._descriptionLabel.visible = false; -+ } -+ } else { -+ this._descriptionLabel.visible = false; -+ } ++ const [item] = this._items.size === 1 ? this.getItems() : []; ++ this._descriptionLabel.text = item?.description ?? ''; + } + + // Override to force visibility even when there's only one item + _updateVisibility() { + } -+}); ++} diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml index ee6b77d322..254d2af958 100644 --- a/js/js-resources.gresource.xml @@ -2644,10 +3247,10 @@ index ee6b77d322..254d2af958 100644 gdm/batch.js gdm/const.js diff --git a/js/ui/popupMenu.js b/js/ui/popupMenu.js -index 2c882c1067..e559845f4c 100644 +index 0426ce5452..b719b1bc0c 100644 --- a/js/ui/popupMenu.js +++ b/js/ui/popupMenu.js -@@ -1105,6 +1105,28 @@ export class PopupMenu extends PopupMenuBase { +@@ -1104,6 +1104,28 @@ export class PopupMenu extends PopupMenuBase { this._boxPointer.setSourceAlignment(alignment); } @@ -2680,10 +3283,10 @@ index 2c882c1067..e559845f4c 100644 2.53.0 -From 6382cd3143c867ee92fbacca4b830ff1a2d4f2e8 Mon Sep 17 00:00:00 2001 +From a4272f28a818a1cb49b51e62a42198bb49c8fa42 Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Tue, 6 Feb 2024 11:11:32 -0500 -Subject: [PATCH 15/30] loginDialog: Port sessions menu over to AuthMenuButton +Subject: [PATCH 26/42] loginDialog: Port sessions menu over to AuthMenuButton Now that AuthMenuButton exists, we should use it. @@ -2695,7 +3298,7 @@ control. 2 files changed, 56 insertions(+), 115 deletions(-) diff --git a/data/theme/gnome-shell-sass/widgets/_login-lock.scss b/data/theme/gnome-shell-sass/widgets/_login-lock.scss -index 4b4f1a5d4e..58c2ed495c 100644 +index 45d47f0652..c18a6bb8a8 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; @@ -2707,7 +3310,7 @@ index 4b4f1a5d4e..58c2ed495c 100644 @extend .icon-button; @extend %system_button; diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js -index d9194cb9fe..bbda3260b5 100644 +index 3a22bf161b..46e2fbc126 100644 --- a/js/gdm/loginDialog.js +++ b/js/gdm/loginDialog.js @@ -27,9 +27,9 @@ import Pango from 'gi://Pango'; @@ -2721,15 +3324,15 @@ index d9194cb9fe..bbda3260b5 100644 import * as CtrlAltTab from '../ui/ctrlAltTab.js'; import * as GdmUtil from './util.js'; import * as Layout from '../ui/layout.js'; -@@ -47,6 +47,7 @@ const _FADE_ANIMATION_TIME = 250; - const _SCROLL_ANIMATION_TIME = 500; +@@ -48,6 +48,7 @@ const _SCROLL_ANIMATION_TIME = 500; + const _FIXED_TOP_ACTOR_HEIGHT = 400; const _TIMED_LOGIN_IDLE_THRESHOLD = 5.0; const _CONFLICTING_SESSION_DIALOG_TIMEOUT = 60; +const _SESSION_TYPE_SECTION_NAME = _('Session Type'); const N_A11Y_MENU_COLUMNS = 2; -@@ -314,103 +315,6 @@ const UserList = GObject.registerClass({ +@@ -315,103 +316,6 @@ const UserList = GObject.registerClass({ } }); @@ -2833,7 +3436,7 @@ index d9194cb9fe..bbda3260b5 100644 const A11yMenuButton = GObject.registerClass( class A11yMenuButton extends St.Button { constructor() { -@@ -591,6 +495,7 @@ export const LoginDialog = GObject.registerClass({ +@@ -592,6 +496,7 @@ export const LoginDialog = GObject.registerClass({ this._authPrompt.connect('prompted', this._onPrompted.bind(this)); this._authPrompt.connect('reset', this._onReset.bind(this)); this._authPrompt.connect('verification-complete', this._onVerificationComplete.bind(this)); @@ -2841,7 +3444,7 @@ index d9194cb9fe..bbda3260b5 100644 this._authPrompt.hide(); this.add_child(this._authPrompt); -@@ -644,14 +549,7 @@ export const LoginDialog = GObject.registerClass({ +@@ -645,14 +550,7 @@ export const LoginDialog = GObject.registerClass({ }); this.add_child(this._bottomButtonGroup); @@ -2857,7 +3460,7 @@ index d9194cb9fe..bbda3260b5 100644 this._a11yMenuButton = new A11yMenuButton(); this._bottomButtonGroup.add_child(this._a11yMenuButton); -@@ -687,6 +585,45 @@ export const LoginDialog = GObject.registerClass({ +@@ -688,6 +586,45 @@ export const LoginDialog = GObject.registerClass({ this._updateDisableUserList.bind(this), this); } @@ -2903,7 +3506,7 @@ index d9194cb9fe..bbda3260b5 100644 _getBannerAllocation(dialogBox) { let actorBox = new Clutter.ActorBox(); -@@ -1057,10 +994,8 @@ export const LoginDialog = GObject.registerClass({ +@@ -1061,10 +998,8 @@ export const LoginDialog = GObject.registerClass({ } _onPrompted() { @@ -2915,15 +3518,15 @@ index d9194cb9fe..bbda3260b5 100644 this._showPrompt(); } -@@ -1087,7 +1022,6 @@ export const LoginDialog = GObject.registerClass({ +@@ -1091,7 +1026,6 @@ export const LoginDialog = GObject.registerClass({ - _onReset(authPrompt, beginRequest) { + _onReset(authPrompt, resetType) { this._ensureGreeterProxy(); - this._sessionMenuButton.updateSensitivity(true); const previousUser = this._user; this._user = null; -@@ -1121,11 +1055,18 @@ export const LoginDialog = GObject.registerClass({ +@@ -1125,11 +1059,18 @@ export const LoginDialog = GObject.registerClass({ }); } @@ -2944,16 +3547,16 @@ index d9194cb9fe..bbda3260b5 100644 const visibleStatuses = [ AuthPrompt.AuthPromptStatus.VERIFYING, AuthPrompt.AuthPromptStatus.VERIFICATION_FAILED, -@@ -1187,7 +1128,7 @@ export const LoginDialog = GObject.registerClass({ +@@ -1191,7 +1132,7 @@ export const LoginDialog = GObject.registerClass({ }); this._updateCancelButton(); - this._sessionMenuButton.updateSensitivity(false); + this._authMenuButton.updateSensitivity(false); - this._authPrompt.updateSensitivity(true); + this._authPrompt.updateSensitivity({sensitive: true}); this._showPrompt(); } -@@ -1501,8 +1442,7 @@ export const LoginDialog = GObject.registerClass({ +@@ -1505,8 +1446,7 @@ export const LoginDialog = GObject.registerClass({ this._ensureUserListLoaded(); this._authPrompt.hide(); this._hideBannerView(); @@ -2967,10 +3570,10 @@ index d9194cb9fe..bbda3260b5 100644 2.53.0 -From e331a873aade48c0ad4bc28103c62716ede9924f Mon Sep 17 00:00:00 2001 +From c4d9263b2270b43fb6f538243277f90559fb8f57 Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Tue, 6 Feb 2024 13:40:26 -0500 -Subject: [PATCH 16/30] loginDialog: Add login options menu to AuthMenuButton +Subject: [PATCH 27/42] loginDialog: Add login options menu to AuthMenuButton MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @@ -2997,7 +3600,7 @@ Co-authored-by: Marco Trevisan (Treviño) 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index 72a1c0a04c..4b18a09563 100644 +index 7b3aa8558f..11d90a4de3 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js @@ -51,6 +51,7 @@ export const AuthPrompt = GObject.registerClass({ @@ -3008,16 +3611,16 @@ index 72a1c0a04c..4b18a09563 100644 'reset': {param_types: [GObject.TYPE_UINT]}, 'verification-complete': {}, 'loading': {param_types: [GObject.TYPE_BOOLEAN]}, -@@ -86,6 +87,7 @@ export const AuthPrompt = GObject.registerClass({ - this._userVerifier.connect('ask-question', this._onAskQuestion.bind(this)); - this._userVerifier.connect('show-message', this._onShowMessage.bind(this)); - this._userVerifier.connect('show-choice-list', this._onShowChoiceList.bind(this)); -+ this._userVerifier.connect('mechanisms-changed', (_, ...args) => this.emit('mechanisms-changed', ...args)); - this._userVerifier.connect('verification-failed', this._onVerificationFailed.bind(this)); - this._userVerifier.connect('verification-complete', this._onVerificationComplete.bind(this)); - this._userVerifier.connect('reset', this._onReset.bind(this)); -@@ -751,6 +753,17 @@ export const AuthPrompt = GObject.registerClass({ - this._updateEntry(false); +@@ -87,6 +88,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), ++ 'mechanisms-changed', (_, ...args) => this.emit('mechanisms-changed', ...args), + 'verification-failed', this._onVerificationFailed.bind(this), + 'verification-complete', this._onVerificationComplete.bind(this), + 'reset', this._onReset.bind(this), +@@ -772,6 +774,17 @@ export const AuthPrompt = GObject.registerClass({ + this.updateSensitivity(false); } + selectMechanism(mechanism) { @@ -3031,22 +3634,22 @@ index 72a1c0a04c..4b18a09563 100644 + return true; + } + - _onUserStoppedTypePreemptiveAnswer() { - if (!this._preemptiveAnswerWatchId || - this._preemptiveAnswer || + reset(params) { + let {reuseEntryText, softReset} = Params.parse(params, { + reuseEntryText: false, diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js -index bbda3260b5..70c461f3a6 100644 +index 46e2fbc126..ddb7e603b2 100644 --- a/js/gdm/loginDialog.js +++ b/js/gdm/loginDialog.js -@@ -47,6 +47,7 @@ const _FADE_ANIMATION_TIME = 250; - const _SCROLL_ANIMATION_TIME = 500; +@@ -48,6 +48,7 @@ const _SCROLL_ANIMATION_TIME = 500; + const _FIXED_TOP_ACTOR_HEIGHT = 400; const _TIMED_LOGIN_IDLE_THRESHOLD = 5.0; const _CONFLICTING_SESSION_DIALOG_TIMEOUT = 60; +const _PRIMARY_LOGIN_METHOD_SECTION_NAME = _('Login Options'); const _SESSION_TYPE_SECTION_NAME = _('Session Type'); const N_A11Y_MENU_COLUMNS = 2; -@@ -496,6 +497,7 @@ export const LoginDialog = GObject.registerClass({ +@@ -497,6 +498,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)); @@ -3054,7 +3657,7 @@ index bbda3260b5..70c461f3a6 100644 this._authPrompt.hide(); this.add_child(this._authPrompt); -@@ -589,18 +591,13 @@ export const LoginDialog = GObject.registerClass({ +@@ -590,18 +592,13 @@ export const LoginDialog = GObject.registerClass({ this._authMenuButton = new AuthMenuButton.AuthMenuButton({ title: _('Login Options'), iconName: 'cog-wheel-symbolic', @@ -3074,7 +3677,7 @@ index bbda3260b5..70c461f3a6 100644 for (const id of ids) { const [sessionName, _] = Gdm.get_session_name_and_description(id); -@@ -616,7 +613,9 @@ export const LoginDialog = GObject.registerClass({ +@@ -617,7 +614,9 @@ export const LoginDialog = GObject.registerClass({ if (!item) return; @@ -3085,7 +3688,7 @@ index bbda3260b5..70c461f3a6 100644 this._greeter.call_select_session_sync(item.id, null); this._authMenuButton.close(); -@@ -624,6 +623,20 @@ export const LoginDialog = GObject.registerClass({ +@@ -625,6 +624,20 @@ export const LoginDialog = GObject.registerClass({ this._bottomButtonGroup.add_child(this._authMenuButton); } @@ -3106,7 +3709,7 @@ index bbda3260b5..70c461f3a6 100644 _getBannerAllocation(dialogBox) { let actorBox = new Clutter.ActorBox(); -@@ -1059,6 +1072,27 @@ export const LoginDialog = GObject.registerClass({ +@@ -1063,6 +1076,27 @@ export const LoginDialog = GObject.registerClass({ this._authMenuButton.updateReactive(!isLoading); } @@ -3138,10 +3741,10 @@ index bbda3260b5..70c461f3a6 100644 2.53.0 -From b42cce9f69d6c488d82e3fa86ce997155cfd27be Mon Sep 17 00:00:00 2001 +From 25db638e4f11984258ed9efebb5ead49f01045cb Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Tue, 6 Feb 2024 13:41:39 -0500 -Subject: [PATCH 17/30] unlockDialog: Add _authMenuButton and +Subject: [PATCH 28/42] unlockDialog: Add _authMenuButton and _authIndicatorButton _authMenuButton is used to select an available auth mechanism from the @@ -3155,11 +3758,11 @@ the bottom right corner of the screen, with _authMenuButton. Only show _otherUserButton when clock is dismissed and authentication prompt visible. --- - js/ui/unlockDialog.js | 157 +++++++++++++++++++++++++++++++++++++----- - 1 file changed, 140 insertions(+), 17 deletions(-) + js/ui/unlockDialog.js | 158 +++++++++++++++++++++++++++++++++++++----- + 1 file changed, 141 insertions(+), 17 deletions(-) diff --git a/js/ui/unlockDialog.js b/js/ui/unlockDialog.js -index a8bdb93b8c..372baee262 100644 +index fffc58e07c..af5a238d87 100644 --- a/js/ui/unlockDialog.js +++ b/js/ui/unlockDialog.js @@ -15,11 +15,16 @@ import * as Main from './main.js'; @@ -3179,7 +3782,7 @@ index a8bdb93b8c..372baee262 100644 // The timeout before going back automatically to the lock screen (in seconds) const IDLE_TIMEOUT = 2 * 60; -@@ -430,12 +435,13 @@ class UnlockDialogClock extends St.BoxLayout { +@@ -432,12 +437,13 @@ class UnlockDialogClock extends St.BoxLayout { const UnlockDialogLayout = GObject.registerClass( class UnlockDialogLayout extends Clutter.LayoutManager { @@ -3195,7 +3798,7 @@ index a8bdb93b8c..372baee262 100644 } vfunc_get_preferred_width(container, forHeight) { -@@ -495,22 +501,40 @@ class UnlockDialogLayout extends Clutter.LayoutManager { +@@ -497,22 +503,40 @@ class UnlockDialogLayout extends Clutter.LayoutManager { this._stack.allocate(actorBox); @@ -3245,7 +3848,7 @@ index a8bdb93b8c..372baee262 100644 } } }); -@@ -620,19 +644,47 @@ export const UnlockDialog = GObject.registerClass({ +@@ -622,19 +646,48 @@ export const UnlockDialog = GObject.registerClass({ this._notificationsBox = new NotificationsBox(); this._notificationsBox.connect('wake-up-screen', () => this.emit('wake-up-screen')); @@ -3288,6 +3891,7 @@ index a8bdb93b8c..372baee262 100644 + // Auth Indicators + this._authIndicatorButton = new AuthMenuButton.AuthMenuButtonIndicator({ + title: _('Background Authentication Methods'), ++ animateVisibility: true, + }); + this._authIndicatorButton.add_style_class_name('login-dialog-bottom-button-group'); + this._authIndicatorButton.set_pivot_point(0.5, 0.5); @@ -3295,7 +3899,7 @@ index a8bdb93b8c..372baee262 100644 this._screenSaverSettings = new Gio.Settings({schema_id: 'org.gnome.desktop.screensaver'}); -@@ -655,11 +707,13 @@ export const UnlockDialog = GObject.registerClass({ +@@ -657,11 +710,13 @@ export const UnlockDialog = GObject.registerClass({ mainBox.add_constraint(new Layout.MonitorConstraint({primary: true})); mainBox.add_child(this._stack); mainBox.add_child(this._notificationsBox); @@ -3311,7 +3915,7 @@ index a8bdb93b8c..372baee262 100644 this.add_child(mainBox); this._idleMonitor = global.backend.get_core_idle_monitor(); -@@ -697,6 +751,20 @@ export const UnlockDialog = GObject.registerClass({ +@@ -699,6 +754,20 @@ export const UnlockDialog = GObject.registerClass({ return Clutter.EVENT_PROPAGATE; } @@ -3332,7 +3936,7 @@ index a8bdb93b8c..372baee262 100644 _createBackground(monitorIndex) { let monitor = Main.layoutManager.monitors[monitorIndex]; let widget = new St.Widget({ -@@ -753,6 +821,8 @@ export const UnlockDialog = GObject.registerClass({ +@@ -755,6 +824,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)); @@ -3341,7 +3945,7 @@ index a8bdb93b8c..372baee262 100644 this._promptBox.add_child(this._authPrompt); } -@@ -815,6 +885,12 @@ export const UnlockDialog = GObject.registerClass({ +@@ -817,6 +888,12 @@ export const UnlockDialog = GObject.registerClass({ reactive: progress > 0, can_focus: progress > 0, }); @@ -3354,7 +3958,7 @@ index a8bdb93b8c..372baee262 100644 const {scaleFactor} = St.ThemeContext.get_for_stage(global.stage); -@@ -832,7 +908,7 @@ export const UnlockDialog = GObject.registerClass({ +@@ -834,7 +911,7 @@ export const UnlockDialog = GObject.registerClass({ translation_y: -FADE_OUT_TRANSLATION * progress * scaleFactor, }); @@ -3363,7 +3967,7 @@ index a8bdb93b8c..372baee262 100644 opacity: 255 * progress, scale_x: FADE_OUT_SCALE + (1 - FADE_OUT_SCALE) * progress, scale_y: FADE_OUT_SCALE + (1 - FADE_OUT_SCALE) * progress, -@@ -856,6 +932,52 @@ export const UnlockDialog = GObject.registerClass({ +@@ -858,6 +935,52 @@ export const UnlockDialog = GObject.registerClass({ this._authPrompt.begin({userName}); } @@ -3416,7 +4020,7 @@ index a8bdb93b8c..372baee262 100644 _escape() { if (this._authPrompt && this.allowCancel) this._authPrompt.cancel(); -@@ -919,7 +1041,8 @@ export const UnlockDialog = GObject.registerClass({ +@@ -921,7 +1044,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') && @@ -3430,10 +4034,10 @@ index a8bdb93b8c..372baee262 100644 2.53.0 -From f67e0659caebe427d4abbf5ede2352a135ce07e1 Mon Sep 17 00:00:00 2001 +From 8256dce648e05285a4bf40b98d547e585f6e3d06 Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Tue, 3 Dec 2024 07:39:32 -0500 -Subject: [PATCH 18/30] unlockDialog: Update hint text based on mockup +Subject: [PATCH 29/42] 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. @@ -3442,10 +4046,10 @@ smartcard and passkey. And have special hint texts. 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/js/ui/unlockDialog.js b/js/ui/unlockDialog.js -index 372baee262..5deb435daa 100644 +index af5a238d87..80c3dd813a 100644 --- a/js/ui/unlockDialog.js +++ b/js/ui/unlockDialog.js -@@ -421,9 +421,17 @@ class UnlockDialogClock extends St.BoxLayout { +@@ -423,9 +423,17 @@ class UnlockDialogClock extends St.BoxLayout { } _updateHint() { @@ -3470,31 +4074,36 @@ index 372baee262..5deb435daa 100644 2.53.0 -From ed34ba8309f1abbbbf684f6f30b6fc921377c83d Mon Sep 17 00:00:00 2001 +From 52f38a7b2d63a96f3af2e1ec87f3832821094fdd Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Tue, 14 Oct 2025 19:19:15 +0200 -Subject: [PATCH 19/30] gdm/util: Increase time of messages based on new +Subject: [PATCH 30/42] gdm/util: Increase time of messages based on new environment variable called 'GDM_MESSAGE_TIME_MULTIPLIER'. This is can used for testing purposes. When no set, the multiplier is 1 which does nothing. --- - js/gdm/util.js | 5 ++++- - 1 file changed, 4 insertions(+), 1 deletion(-) + js/gdm/util.js | 4 +++- + 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/js/gdm/util.js b/js/gdm/util.js -index cabd7cbf02..6f19d03989 100644 +index 660c6e4ddc..12dec5dd46 100644 --- a/js/gdm/util.js +++ b/js/gdm/util.js -@@ -278,8 +278,11 @@ export class ShellUserVerifier extends Signals.EventEmitter { - if (!message) +@@ -43,6 +43,7 @@ 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 FINGERPRINT_SERVICE_PROXY_TIMEOUT = 5000; + const FINGERPRINT_ERROR_TIMEOUT_WAIT = 15; +@@ -278,7 +279,8 @@ export class ShellUserVerifier extends Signals.EventEmitter { return 0; -+ const messageTimeMultiplier = GLib.getenv('GDM_MESSAGE_TIME_MULTIPLIER') ?? 1; -+ // 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 * messageTimeMultiplier, ++ return Math.max(message.length * USER_READ_TIME * MESSAGE_TIME_MULTIPLIER, + USER_READ_TIME_MIN); } @@ -3503,10 +4112,10 @@ index cabd7cbf02..6f19d03989 100644 2.53.0 -From ae8dd287a062d83332f0750d65d56177b93b0a4c Mon Sep 17 00:00:00 2001 +From b49076f274bc33e32e52747e6858d28fb029a1fc Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Mon, 15 Sep 2025 17:13:17 +0200 -Subject: [PATCH 20/30] gdm/util: Allow null _hold and don't recreate dummy +Subject: [PATCH 31/42] gdm/util: Allow null _hold and don't recreate dummy holds _hold property is used to inform the caller of begin method (authPrompt) @@ -3525,12 +4134,12 @@ 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 4b18a09563..cb851c05b6 100644 +index 11d90a4de3..9a9c0c3802 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -857,11 +857,7 @@ export const AuthPrompt = GObject.registerClass({ +@@ -858,11 +858,7 @@ export const AuthPrompt = GObject.registerClass({ - this.updateSensitivity(false); + this.updateSensitivity({sensitive: false}); - let hold = params.hold; - if (!hold) @@ -3542,10 +4151,10 @@ index 4b18a09563..cb851c05b6 100644 } diff --git a/js/gdm/util.js b/js/gdm/util.js -index 6f19d03989..2f2862f19d 100644 +index 12dec5dd46..247048ddab 100644 --- a/js/gdm/util.js +++ b/js/gdm/util.js -@@ -464,8 +464,6 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -462,8 +462,6 @@ export class ShellUserVerifier extends Signals.EventEmitter { if (this._userVerifier && !this._activeServices.has(Const.FINGERPRINT_SERVICE_NAME)) { @@ -3554,7 +4163,7 @@ index 6f19d03989..2f2862f19d 100644 await this._maybeStartFingerprintVerification(); } } -@@ -529,7 +527,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -527,7 +525,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { _reportInitError(where, error, serviceName) { logError(error, where); @@ -3563,7 +4172,7 @@ index 6f19d03989..2f2862f19d 100644 this._queueMessage(serviceName, _('Authentication error'), MessageType.ERROR); this._failCounter++; -@@ -567,7 +565,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -565,7 +563,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { this.reauthenticating = true; this._connectSignals(); this._beginVerification(); @@ -3572,7 +4181,7 @@ index 6f19d03989..2f2862f19d 100644 } async _getUserVerifier() { -@@ -591,7 +589,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -589,7 +587,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { this._connectSignals(); this._beginVerification(); @@ -3581,7 +4190,7 @@ index 6f19d03989..2f2862f19d 100644 } _connectSignals() { -@@ -709,7 +707,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -707,7 +705,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { } async _startService(serviceName) { @@ -3589,8 +4198,8 @@ index 6f19d03989..2f2862f19d 100644 + this._hold?.acquire(); try { this._activeServices.add(serviceName); - -@@ -730,7 +728,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { + if (this._userName) { +@@ -724,7 +722,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { if (!this.serviceIsForeground(serviceName)) { logError(e, `Failed to start ${serviceName} for ${this._userName}`); @@ -3599,7 +4208,7 @@ index 6f19d03989..2f2862f19d 100644 return; } this._reportInitError( -@@ -740,7 +738,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -734,7 +732,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { e, serviceName); return; } @@ -3608,7 +4217,7 @@ index 6f19d03989..2f2862f19d 100644 } _beginVerification() { -@@ -874,7 +872,6 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -867,7 +865,6 @@ export class ShellUserVerifier extends Signals.EventEmitter { } _retry(serviceName) { @@ -3620,10 +4229,10 @@ index 6f19d03989..2f2862f19d 100644 2.53.0 -From b2f750be73f2c22866a4e9ed356722e0d09c092e Mon Sep 17 00:00:00 2001 +From 50aef73235bd66243f1472a24711840beea5eb27 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Wed, 20 Aug 2025 10:54:33 +0200 -Subject: [PATCH 21/30] gdm/util: Add fingerprintManager +Subject: [PATCH 32/42] gdm/util: Add fingerprintManager Move fingerprint bits to new fingerprintManager class. @@ -3635,12 +4244,12 @@ authentication to a specific class instead of util. --- js/gdm/util.js | 154 +++++++++------------------------- js/js-resources.gresource.xml | 1 + - js/misc/fingerprintManager.js | 122 +++++++++++++++++++++++++++ - 3 files changed, 161 insertions(+), 116 deletions(-) + js/misc/fingerprintManager.js | 140 +++++++++++++++++++++++++++++++ + 3 files changed, 179 insertions(+), 116 deletions(-) create mode 100644 js/misc/fingerprintManager.js diff --git a/js/gdm/util.js b/js/gdm/util.js -index 2f2862f19d..6c9e0a54a2 100644 +index 247048ddab..ae464665e2 100644 --- a/js/gdm/util.js +++ b/js/gdm/util.js @@ -5,6 +5,8 @@ import GLib from 'gi://GLib'; @@ -3664,7 +4273,7 @@ index 2f2862f19d..6c9e0a54a2 100644 Gio._promisify(Gdm.Client.prototype, 'open_reauthentication_channel'); Gio._promisify(Gdm.Client.prototype, 'get_user_verifier'); Gio._promisify(Gdm.UserVerifierProxy.prototype, -@@ -59,12 +56,6 @@ export const MessageType = { +@@ -60,12 +57,6 @@ export const MessageType = { ERROR: 3, }; @@ -3677,16 +4286,16 @@ index 2f2862f19d..6c9e0a54a2 100644 /** * @param {Clutter.Actor} actor */ -@@ -140,7 +131,8 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -141,7 +132,8 @@ export class ShellUserVerifier extends Signals.EventEmitter { this._defaultService = null; this._preemptingService = null; - this._fingerprintReaderType = FingerprintReaderType.NONE; + this._fingerprintReaderType = FingerprintManager.FingerprintReaderType.NONE; + this._fingerprintReaderFound = false; - this._fprintStartTime = -1; this._messageQueue = []; + this._messageQueueTimeoutId = 0; @@ -205,8 +197,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { this._userName = userName; this.reauthenticating = false; @@ -3705,7 +4314,7 @@ index 2f2862f19d..6c9e0a54a2 100644 this._fingerprintManager = null; for (let service in this._credentialManagers) -@@ -368,112 +360,40 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -366,112 +358,40 @@ export class ShellUserVerifier extends Signals.EventEmitter { } async _initFingerprintManager() { @@ -3843,7 +4452,7 @@ index 2f2862f19d..6c9e0a54a2 100644 } _onCredentialManagerAuthenticated(credentialManager, _token) { -@@ -643,7 +563,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -641,7 +561,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { } serviceIsFingerprint(serviceName) { @@ -3852,7 +4461,7 @@ index 2f2862f19d..6c9e0a54a2 100644 serviceName === Const.FINGERPRINT_SERVICE_NAME; } -@@ -656,9 +576,11 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -654,9 +574,11 @@ export class ShellUserVerifier extends Signals.EventEmitter { let needsReset = false; if (this._settings.get_boolean(FINGERPRINT_AUTHENTICATION_KEY)) { @@ -3865,7 +4474,7 @@ index 2f2862f19d..6c9e0a54a2 100644 this._fingerprintReaderType = FingerprintReaderType.NONE; if (this._activeServices.has(Const.FINGERPRINT_SERVICE_NAME)) -@@ -686,7 +608,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -684,7 +606,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { return Const.PASSWORD_SERVICE_NAME; else if (this._smartcardManager) return Const.SMARTCARD_SERVICE_NAME; @@ -3874,7 +4483,7 @@ index 2f2862f19d..6c9e0a54a2 100644 return Const.FINGERPRINT_SERVICE_NAME; return null; } -@@ -748,7 +670,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -742,7 +664,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { async _maybeStartFingerprintVerification() { if (this._userName && @@ -3883,7 +4492,7 @@ index 2f2862f19d..6c9e0a54a2 100644 !this.serviceIsForeground(Const.FINGERPRINT_SERVICE_NAME)) await this._startService(Const.FINGERPRINT_SERVICE_NAME); } -@@ -771,7 +693,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -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. @@ -3906,14 +4515,13 @@ index 254d2af958..2712bfcc84 100644 misc/history.js diff --git a/js/misc/fingerprintManager.js b/js/misc/fingerprintManager.js new file mode 100644 -index 0000000000..60b5163b48 +index 0000000000..5bcb7bf6f0 --- /dev/null +++ b/js/misc/fingerprintManager.js -@@ -0,0 +1,122 @@ +@@ -0,0 +1,140 @@ +import Gio from 'gi://Gio'; +import GLib from 'gi://GLib'; -+ -+import * as Signals from './signals.js'; ++import GObject from 'gi://GObject'; + +import {loadInterfaceXML} from './fileUtils.js'; + @@ -3928,8 +4536,38 @@ index 0000000000..60b5163b48 + SWIPE: 2, +}; + -+export class FingerprintManager extends Signals.EventEmitter { -+ constructor(cancellable) { ++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({ @@ -3943,17 +4581,11 @@ index 0000000000..60b5163b48 + Gio.DBusProxyFlags.DO_NOT_CONNECT_SIGNALS, + }); + -+ this._fingerprintReaderType = FingerprintReaderType.NONE; -+ -+ this._initFingerprintManagerProxy(cancellable); -+ } -+ -+ get readerType() { -+ return this._fingerprintReaderType; ++ this._initFingerprintManagerProxy(); + } + + get readerFound() { -+ return this._fingerprintReaderFound; ++ return this.readerType !== FingerprintReaderType.NONE; + } + + setDefaultTimeout(timeout) { @@ -3974,15 +4606,17 @@ index 0000000000..60b5163b48 + const fingerprintReaderType = FingerprintReaderType[fingerprintReaderKey]; + this._setFingerprintReaderType(fingerprintReaderType); + } catch (e) { ++ if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) ++ return; + this._handleFingerprintError(e); + } + } + -+ async _initFingerprintManagerProxy(cancellable) { ++ async _initFingerprintManagerProxy() { + try { + await this._fingerprintManagerProxy.init_async( -+ GLib.PRIORITY_DEFAULT, cancellable ?? null); -+ await this.checkReaderType(cancellable); ++ GLib.PRIORITY_DEFAULT, null); ++ await this.checkReaderType(); + } catch (e) { + this._handleFingerprintError(e); + } @@ -4000,18 +4634,13 @@ index 0000000000..60b5163b48 + } + + _setFingerprintReaderType(fingerprintReaderType) { -+ if (this._fingerprintReaderType === fingerprintReaderType) -+ return; -+ -+ this._fingerprintReaderType = fingerprintReaderType; -+ -+ this._fingerprintReaderFound = -+ !!this._fingerprintReaderType && -+ this._fingerprintReaderType !== FingerprintReaderType.NONE; -+ -+ if (this._fingerprintReaderType === undefined) ++ if (fingerprintReaderType === undefined) + throw new Error(`Unexpected fingerprint device type '${fingerprintReaderType}'`); + ++ if (this.readerType === fingerprintReaderType) ++ return; ++ ++ this.readerType = fingerprintReaderType; + this.emit('reader-type-changed'); + } + @@ -4019,8 +4648,6 @@ index 0000000000..60b5163b48 + this._setFingerprintReaderType(FingerprintReaderType.NONE); + + if (e instanceof GLib.Error) { -+ if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) -+ return; + if (e.matches(Gio.DBusError, Gio.DBusError.SERVICE_UNKNOWN)) + return; + if (Gio.DBusError.is_remote_error(e) && @@ -4036,10 +4663,10 @@ index 0000000000..60b5163b48 2.53.0 -From f91df3775febb90808c57075896308566cad0928 Mon Sep 17 00:00:00 2001 +From 584b5621794d7aa445f46a366ca43847ac81779d Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Tue, 23 Sep 2025 16:45:59 +0200 -Subject: [PATCH 22/30] gdm/misc: Add passkeyManager +Subject: [PATCH 33/42] gdm/misc: 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. @@ -4047,21 +4674,21 @@ 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/passkeyManager.js | 60 +++++++++++++++++++++++++++++++++++ - 3 files changed, 62 insertions(+) - create mode 100644 js/misc/passkeyManager.js + 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 diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml -index 2712bfcc84..11de814fbd 100644 +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/passkeyManager.js ++ misc/passkeyDeviceManager.js misc/permissionStore.js misc/signals.js misc/signalTracker.js @@ -4077,28 +4704,39 @@ index f8f98d4871..c3c51aa2de 100644 import 'gi://GWeather?version=4.0'; import 'gi://IBus?version=1.0'; import 'gi://Pango?version=1.0'; -diff --git a/js/misc/passkeyManager.js b/js/misc/passkeyManager.js +diff --git a/js/misc/passkeyDeviceManager.js b/js/misc/passkeyDeviceManager.js new file mode 100644 -index 0000000000..66e17e3f3c +index 0000000000..9650ac1d2d --- /dev/null -+++ b/js/misc/passkeyManager.js -@@ -0,0 +1,60 @@ ++++ b/js/misc/passkeyDeviceManager.js +@@ -0,0 +1,71 @@ ++import GObject from 'gi://GObject'; +import GUdev from 'gi://GUdev'; -+import * as Signals from './signals.js'; + -+let _passkeyManager = null; ++let _passkeyDeviceManager = null; + +/** -+ * @returns {PasskeyManager} ++ * @returns {PasskeyDeviceManager} + */ -+export function getPasskeyManager() { -+ if (_passkeyManager == null) -+ _passkeyManager = new PasskeyManager(); ++export function getPasskeyDeviceManager() { ++ if (_passkeyDeviceManager == null) ++ _passkeyDeviceManager = new PasskeyDeviceManager(); + -+ return _passkeyManager; ++ return _passkeyDeviceManager; +} + -+class PasskeyManager extends Signals.EventEmitter { ++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]}, ++ }; ++ ++ static { ++ GObject.registerClass(this); ++ } ++ + constructor() { + super(); + @@ -4108,8 +4746,8 @@ index 0000000000..66e17e3f3c + this._onLoaded(); + } + -+ hasInsertedPasskeys() { -+ return Object.keys(this._insertedPasskeys).length > 0; ++ get hasInsertedPasskeys() { ++ return this._insertedPasskeys.size > 0; + } + + _onLoaded() { @@ -4127,7 +4765,7 @@ index 0000000000..66e17e3f3c + _addPasskey(device) { + const sysfsPath = device.get_sysfs_path(); + const isFido = device.get_property_as_int('ID_FIDO_TOKEN') === 1; -+ if (this._insertedPasskeys.has(sysfsPath) || !isFido) ++ if (!isFido || this._insertedPasskeys.has(sysfsPath)) + return; + + this._insertedPasskeys.set(sysfsPath, device); @@ -4147,10 +4785,10 @@ index 0000000000..66e17e3f3c 2.53.0 -From aed7e75c2f19aee4964038fdb6ba89d69959b278 Mon Sep 17 00:00:00 2001 +From 93c424c183dce14d4aa305fba71ccb2361a3b1dc Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Mon, 18 Aug 2025 12:08:21 +0200 -Subject: [PATCH 23/30] gdm: Add AuthServices +Subject: [PATCH 34/42] gdm: Add AuthServices MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @@ -4208,19 +4846,19 @@ Based on the previous work done by: - Marco Trevisan (Treviño) - Ray Strode --- - js/gdm/authPrompt.js | 42 +- - js/gdm/authServices.js | 465 +++++++++++++++++++++ - js/gdm/authServicesLegacy.js | 318 +++++++++++++++ - js/gdm/util.js | 733 +++++++--------------------------- + js/gdm/authPrompt.js | 44 +-- + js/gdm/authServices.js | 476 ++++++++++++++++++++++ + js/gdm/authServicesLegacy.js | 324 +++++++++++++++ + js/gdm/util.js | 718 +++++++--------------------------- js/js-resources.gresource.xml | 2 + js/misc/fingerprintManager.js | 4 - po/POTFILES.in | 1 + - 7 files changed, 940 insertions(+), 625 deletions(-) + 7 files changed, 961 insertions(+), 608 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 cb851c05b6..10598e5c08 100644 +index 9a9c0c3802..d8c7c6409d 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js @@ -10,7 +10,6 @@ import St from 'gi://St'; @@ -4231,17 +4869,18 @@ index cb851c05b6..10598e5c08 100644 import * as GdmUtil from './util.js'; import * as Params from '../misc/params.js'; import * as ShellEntry from '../ui/shellEntry.js'; -@@ -91,9 +90,6 @@ export const AuthPrompt = GObject.registerClass({ - this._userVerifier.connect('verification-failed', this._onVerificationFailed.bind(this)); - this._userVerifier.connect('verification-complete', this._onVerificationComplete.bind(this)); - this._userVerifier.connect('reset', this._onReset.bind(this)); -- this._userVerifier.connect('smartcard-status-changed', this._onSmartcardStatusChanged.bind(this)); -- this._userVerifier.connect('credential-manager-authenticated', this._onCredentialManagerAuthenticated.bind(this)); +@@ -92,10 +91,7 @@ export const AuthPrompt = GObject.registerClass({ + 'verification-failed', this._onVerificationFailed.bind(this), + 'verification-complete', this._onVerificationComplete.bind(this), + 'reset', this._onReset.bind(this), +- 'smartcard-status-changed', this._onSmartcardStatusChanged.bind(this), +- 'credential-manager-authenticated', this._onCredentialManagerAuthenticated.bind(this), + this); - this.smartcardDetected = this._userVerifier.smartcardDetected; this.connect('destroy', this._onDestroy.bind(this)); -@@ -441,31 +437,6 @@ export const AuthPrompt = GObject.registerClass({ +@@ -445,31 +441,6 @@ export const AuthPrompt = GObject.registerClass({ this.emit('prompted'); } @@ -4273,7 +4912,7 @@ index cb851c05b6..10598e5c08 100644 _onShowMessage(_userVerifier, serviceName, message, type) { let wiggleParameters = {duration: 0}; -@@ -761,6 +732,13 @@ export const AuthPrompt = GObject.registerClass({ +@@ -782,6 +753,13 @@ export const AuthPrompt = GObject.registerClass({ if (invalidStatus.includes(this.verificationStatus)) return false; @@ -4287,7 +4926,7 @@ index cb851c05b6..10598e5c08 100644 return true; } -@@ -799,8 +777,10 @@ export const AuthPrompt = GObject.registerClass({ +@@ -803,8 +781,10 @@ export const AuthPrompt = GObject.registerClass({ this._preemptiveAnswerWatchId = this._idleMonitor.add_idle_watch(3000, this._onUserStoppedTypePreemptiveAnswer.bind(this)); @@ -4298,14 +4937,23 @@ index cb851c05b6..10598e5c08 100644 + else + this._userVerifier?.reset(); - this._queryingService = null; - this.clear(); + reuseEntryText = reuseEntryText || this._preemptiveInput; + +@@ -828,7 +808,7 @@ export const AuthPrompt = GObject.registerClass({ + if (oldStatus === AuthPromptStatus.VERIFICATION_CANCELLED) + return; + resetType = ResetType.PROVIDE_USERNAME; +- } else if (this._userVerifier.foregroundServiceDeterminesUsername()) { ++ } else if (!this._userVerifier.needsUsername()) { + // We don't need to know the username if the user preempted the login screen + // with a smartcard or with preauthenticated oVirt credentials + resetType = ResetType.DONT_PROVIDE_USERNAME; diff --git a/js/gdm/authServices.js b/js/gdm/authServices.js new file mode 100644 -index 0000000000..68a2c4c3f5 +index 0000000000..f702b15a04 --- /dev/null +++ b/js/gdm/authServices.js -@@ -0,0 +1,465 @@ +@@ -0,0 +1,476 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +import * as FingerprintManager from '../misc/fingerprintManager.js'; @@ -4323,8 +4971,10 @@ index 0000000000..68a2c4c3f5 +Gio._promisify(Gdm.UserVerifierProxy.prototype, 'call_begin_verification_for_user'); +Gio._promisify(Gdm.UserVerifierProxy.prototype, 'call_begin_verification'); + -+export const AuthServices = GObject.registerClass({ -+ Signals: { ++export class AuthServices extends GObject.Object { ++ static [GObject.GTypeName] = 'AuthServices'; ++ ++ static [GObject.signals] = { + 'queue-message': { + param_types: [GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_UINT], + }, @@ -4351,16 +5001,21 @@ index 0000000000..68a2c4c3f5 + param_types: [GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_JSOBJECT], + }, + 'mechanisms-changed': {}, -+ }, -+}, class AuthServices extends GObject.Object { ++ }; ++ ++ static { ++ GObject.registerClass(this); ++ } ++ + static SupportedRoles = []; + static RoleToService = {}; ++ + static supportsAny(roles) { + return roles.some(r => this.SupportedRoles.includes(r)); + } + -+ _init(params) { -+ super._init(); ++ constructor(params) { ++ super(); + params = Params.parse(params, { + client: null, + enabledRoles: [], @@ -4413,13 +5068,13 @@ index 0000000000..68a2c4c3f5 + } + + async beginVerification(userName, userVerifierProxies) { -+ if (!this._cancellable) -+ this._cancellable = new Gio.Cancellable(); ++ this._cancellable?.cancel(); ++ this._cancellable = new Gio.Cancellable(); + this._userName = userName; + + try { + this._updateUserVerifier(userVerifierProxies); -+ await this._startServices(); ++ await this._startServices(this._cancellable); + } catch (e) { + if (e.error?.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + return; @@ -4509,7 +5164,7 @@ index 0000000000..68a2c4c3f5 + if (!this._reauthOnly) + return; + -+ this._fingerprintManager = new FingerprintManager.FingerprintManager(); ++ this._fingerprintManager = FingerprintManager.getFingerprintManager(); + this._fingerprintManager.connectObject( + 'reader-type-changed', () => this._handleFingerprintChanged(), + this); @@ -4661,11 +5316,11 @@ index 0000000000..68a2c4c3f5 + } + } + -+ async _startServices() { ++ async _startServices(cancellable) { + for (const serviceName of this._getEnabledServices()) { + if (this._canStartService(serviceName)) { + // eslint-disable-next-line no-await-in-loop -+ await this._startService(serviceName); ++ await this._startService(serviceName, cancellable); + } + } + } @@ -4691,15 +5346,15 @@ index 0000000000..68a2c4c3f5 + this._handleCanStartService(serviceName); + } + -+ async _startService(serviceName) { ++ async _startService(serviceName, cancellable) { + try { + this._activeServices.add(serviceName); + if (this._userName) { + await this._userVerifier.call_begin_verification_for_user( -+ serviceName, this._userName, this._cancellable); ++ serviceName, this._userName, cancellable); + } else { + await this._userVerifier.call_begin_verification( -+ serviceName, this._cancellable); ++ serviceName, cancellable); + } + } catch (e) { + this._activeServices.delete(serviceName); @@ -4735,7 +5390,10 @@ index 0000000000..68a2c4c3f5 + + _handleClear() {} + -+ _handleUpdateEnabledMechanisms() {} ++ _handleUpdateEnabledMechanisms() { ++ throw new GObject.NotImplementedError( ++ `_handleUpdateEnabledMechanisms in ${this.constructor.name}`); ++ } + + _handleSmartcardChanged() {} + @@ -4768,15 +5426,16 @@ index 0000000000..68a2c4c3f5 + } + + _handleCanStartService() { -+ return false; ++ throw new GObject.NotImplementedError( ++ `_handleCanStartService in ${this.constructor.name}`); + } -+}); ++} diff --git a/js/gdm/authServicesLegacy.js b/js/gdm/authServicesLegacy.js new file mode 100644 -index 0000000000..93f55ec45d +index 0000000000..e9c1676ab9 --- /dev/null +++ b/js/gdm/authServicesLegacy.js -@@ -0,0 +1,318 @@ +@@ -0,0 +1,324 @@ +import GLib from 'gi://GLib'; +import GObject from 'gi://GObject'; + @@ -4807,8 +5466,9 @@ index 0000000000..93f55ec45d + }, +]; + -+export const AuthServicesLegacy = GObject.registerClass({ -+}, class AuthServicesLegacy extends AuthServices { ++export class AuthServicesLegacy extends AuthServices { ++ static [GObject.GTypeName] = 'AuthServicesLegacy'; ++ + static SupportedRoles = [ + Const.PASSWORD_ROLE_NAME, + Const.SMARTCARD_ROLE_NAME, @@ -4821,8 +5481,12 @@ index 0000000000..93f55ec45d + [Const.FINGERPRINT_ROLE_NAME]: Const.FINGERPRINT_SERVICE_NAME, + }; + -+ _init(params) { -+ super._init(params); ++ static { ++ GObject.registerClass(this); ++ } ++ ++ constructor(params) { ++ super(params); + + this._updateEnabledMechanisms(); + @@ -5006,15 +5670,16 @@ index 0000000000..93f55ec45d + return; + } + -+ if (this._unavailableServices.has(serviceName)) { -+ if (serviceName === Const.FINGERPRINT_SERVICE_NAME) { -+ this._enabledMechanisms = this._enabledMechanisms -+ .filter(m => m.serviceName !== serviceName); -+ this.emit('mechanisms-changed'); -+ } -+ return; ++ if (serviceName === Const.FINGERPRINT_SERVICE_NAME && ++ this._unavailableServices.has(serviceName)) { ++ this._enabledMechanisms = this._enabledMechanisms ++ .filter(m => m.serviceName !== serviceName); ++ this.emit('mechanisms-changed'); + } + ++ if (this._unavailableServices.has(serviceName)) ++ return; ++ + // if the password service fails, then cancel everything. + // But if, e.g., fingerprint fails, still give + // password authentication a chance to succeed @@ -5056,9 +5721,9 @@ index 0000000000..93f55ec45d + if (serviceName !== this._selectedMechanism?.serviceName) + return; + -+ const choiceList = Object.fromEntries( -+ Object.entries(list.deepUnpack()) -+ .map(([key, value]) => [key, {description: value}])); ++ const choiceList = {}; ++ for (const [key, value] of Object.entries(list.deepUnpack())) ++ choiceList[key] = {title: value}; + + this.emit('show-choice-list', serviceName, promptMessage, choiceList); + } @@ -5094,9 +5759,9 @@ index 0000000000..93f55ec45d + }; + this.emit('reset', {softReset: true}); + } -+}); ++} diff --git a/js/gdm/util.js b/js/gdm/util.js -index 6c9e0a54a2..4c95fd0b6f 100644 +index ae464665e2..8c11c63c49 100644 --- a/js/gdm/util.js +++ b/js/gdm/util.js @@ -1,25 +1,13 @@ @@ -5126,9 +5791,9 @@ index 6c9e0a54a2..4c95fd0b6f 100644 const CLONE_FADE_ANIMATION_TIME = 250; -@@ -41,9 +29,6 @@ export const DISABLE_USER_LIST_KEY = 'disable-user-list'; - const USER_READ_TIME = 48; +@@ -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; -const FINGERPRINT_SERVICE_PROXY_TIMEOUT = 5000; -const FINGERPRINT_ERROR_TIMEOUT_WAIT = 15; @@ -5136,7 +5801,7 @@ index 6c9e0a54a2..4c95fd0b6f 100644 /** * Keep messages in order by priority * -@@ -56,6 +41,15 @@ export const MessageType = { +@@ -57,6 +42,15 @@ export const MessageType = { ERROR: 3, }; @@ -5152,7 +5817,7 @@ index 6c9e0a54a2..4c95fd0b6f 100644 /** * @param {Clutter.Actor} actor */ -@@ -129,54 +123,12 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -130,53 +124,12 @@ export class ShellUserVerifier extends Signals.EventEmitter { this._client = client; this._cancellable = null; @@ -5160,7 +5825,6 @@ index 6c9e0a54a2..4c95fd0b6f 100644 - this._preemptingService = null; - this._fingerprintReaderType = FingerprintManager.FingerprintReaderType.NONE; - this._fingerprintReaderFound = false; -- this._fprintStartTime = -1; - this._messageQueue = []; this._messageQueueTimeoutId = 0; @@ -5208,12 +5872,13 @@ index 6c9e0a54a2..4c95fd0b6f 100644 } get hasPendingMessages() { -@@ -191,50 +143,54 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -191,79 +144,72 @@ export class ShellUserVerifier extends Signals.EventEmitter { return this._messageQueue ? this._messageQueue[0] : null; } - begin(userName, hold) { + async begin(userName, hold) { ++ this._cancellable?.cancel(); this._cancellable = new Gio.Cancellable(); - this._hold = hold; - this._userName = userName; @@ -5221,7 +5886,7 @@ index 6c9e0a54a2..4c95fd0b6f 100644 - this._fingerprintManager?.checkReaderType(this._cancellable); + try { -+ const proxies = await this._getUserVerifierProxies(userName); ++ const proxies = await this._getUserVerifierProxies(userName, this._cancellable); + await this._authServicesLegacy?.beginVerification(userName, proxies); + this._userVerifier = proxies.userVerifier; + } catch (e) { @@ -5294,7 +5959,9 @@ index 6c9e0a54a2..4c95fd0b6f 100644 } destroy() { -@@ -242,28 +198,14 @@ export class ShellUserVerifier extends Signals.EventEmitter { ++ this._authServicesLegacy?.disconnectObject(this); ++ + this.cancel(); this._settings.run_dispose(); this._settings = null; @@ -5326,7 +5993,7 @@ index 6c9e0a54a2..4c95fd0b6f 100644 } _getIntervalForMessage(message) { -@@ -277,7 +219,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -275,7 +221,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { USER_READ_TIME_MIN); } @@ -5335,7 +6002,7 @@ index 6c9e0a54a2..4c95fd0b6f 100644 if (!this.hasPendingMessages) return; -@@ -320,7 +262,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -318,7 +264,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { this._messageQueue.shift(); this._queueMessageTimeout(); } else { @@ -5344,7 +6011,7 @@ index 6c9e0a54a2..4c95fd0b6f 100644 } return GLib.SOURCE_REMOVE; -@@ -350,7 +292,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -348,7 +294,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { } _clearMessageQueue() { @@ -5353,7 +6020,7 @@ index 6c9e0a54a2..4c95fd0b6f 100644 if (this._messageQueueTimeoutId !== 0) { GLib.source_remove(this._messageQueueTimeoutId); -@@ -359,488 +301,142 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -357,471 +303,142 @@ export class ShellUserVerifier extends Signals.EventEmitter { this.emit('show-message', null, null, MessageType.NONE); } @@ -5421,7 +6088,9 @@ index 6c9e0a54a2..4c95fd0b6f 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) @@ -5431,9 +6100,7 @@ index 6c9e0a54a2..4c95fd0b6f 100644 - - if (smartcardDetected !== this.smartcardDetected) { - this.smartcardDetected = smartcardDetected; -+ _reportInitError(initError) { -+ const {error, where, serviceName} = initError; - +- - if (this.smartcardDetected) - this._preemptingService = Const.SMARTCARD_SERVICE_NAME; - else if (this._preemptingService === Const.SMARTCARD_SERVICE_NAME) @@ -5471,13 +6138,13 @@ index 6c9e0a54a2..4c95fd0b6f 100644 - // verification from this login session - this._getUserVerifier(); - return; -+ async _getUserVerifierProxies(userName) { ++ async _getUserVerifierProxies(userName, cancellable) { + const proxies = {}; + + if (userName) { + try { + proxies.userVerifier = await this._client.open_reauthentication_channel( -+ userName, this._cancellable); ++ userName, cancellable); + } catch (e) { + if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + throw e; @@ -5486,14 +6153,14 @@ index 6c9e0a54a2..4c95fd0b6f 100644 + // Gdm emits org.freedesktop.DBus.Error.AccessDenied when there + // is no session to reauthenticate. Fall back to performing + // verification from this login session -+ return this._getUserVerifierProxies(); ++ return this._getUserVerifierProxies(null, cancellable); + } + throw new InitError(e, 'Failed to open reauthentication channel'); + } + } else { + try { + proxies.userVerifier = await this._client.get_user_verifier( -+ this._cancellable); ++ cancellable); + } catch (e) { + if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + throw e; @@ -5531,8 +6198,9 @@ index 6c9e0a54a2..4c95fd0b6f 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 @@ -5584,9 +6252,8 @@ index 6c9e0a54a2..4c95fd0b6f 100644 - for (let serviceName in this._credentialManagers) { - if (this.serviceIsForeground(serviceName)) - return true; -+ throw new InitError(e, 'Failed to obtain user verifier extensions'); - } - +- } +- - return this.serviceIsForeground(Const.SMARTCARD_SERVICE_NAME); - } - @@ -5633,9 +6300,8 @@ index 6c9e0a54a2..4c95fd0b6f 100644 - - if (needsReset) - this._cancelAndReset(); -+ this._updateAuthServices(); - } - +- } +- - _getDetectedDefaultService() { - if (this._smartcardManager?.loggedInWithToken()) - return Const.SMARTCARD_SERVICE_NAME; @@ -5646,13 +6312,25 @@ index 6c9e0a54a2..4c95fd0b6f 100644 - else if (this._fingerprintReaderFound) - return Const.FINGERPRINT_SERVICE_NAME; - return null; -- } -+ _updateAuthServices() { -+ const enabledRoles = []; ++ this._updateAuthServices(); + } - _updateDefaultService() { - const oldDefaultService = this._defaultService; - this._defaultService = this._getDetectedDefaultService(); +- +- if (!this._defaultService) { +- log('no authentication service is enabled, using password authentication'); +- this._defaultService = Const.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)) @@ -5660,25 +6338,10 @@ index 6c9e0a54a2..4c95fd0b6f 100644 + if (this._settings.get_boolean(FINGERPRINT_AUTHENTICATION_KEY)) + enabledRoles.push(Const.FINGERPRINT_ROLE_NAME); -- if (!this._defaultService) { -- log('no authentication service is enabled, using password authentication'); -- this._defaultService = Const.PASSWORD_SERVICE_NAME; -- } -- -- if (oldDefaultService && -- oldDefaultService !== this._defaultService && -- this._activeServices.has(oldDefaultService)) -- this._cancelAndReset(); -- } -- - async _startService(serviceName) { - this._hold?.acquire(); - try { - this._activeServices.add(serviceName); -- -- if (serviceName == FINGERPRINT_SERVICE_NAME) -- this._fprintStartTime = GLib.get_monotonic_time(); -- - if (this._userName) { - await this._userVerifier.call_begin_verification_for_user( - serviceName, this._userName, this._cancellable); @@ -5725,9 +6388,9 @@ index 6c9e0a54a2..4c95fd0b6f 100644 - if (!this.serviceIsForeground(serviceName)) - return; - -- const choiceList = Object.fromEntries( -- Object.entries(list.deepUnpack()) -- .map(([key, value]) => [key, {description: value}])); +- const choiceList = {}; +- for (const [key, value] of Object.entries(list.deepUnpack())) +- choiceList[key] = {title: value}; - - this.emit('show-choice-list', serviceName, promptMessage, choiceList); - } @@ -5780,7 +6443,6 @@ index 6c9e0a54a2..4c95fd0b6f 100644 - const cancellable = this._cancellable; - this._fingerprintFailedId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, - FINGERPRINT_ERROR_TIMEOUT_WAIT, () => { -- log("Generating _verificationFailed!"); - this._fingerprintFailedId = 0; - if (!cancellable.is_cancelled()) - this._verificationFailed(serviceName, false); @@ -5892,18 +6554,6 @@ index 6c9e0a54a2..4c95fd0b6f 100644 - if (serviceName === Const.FINGERPRINT_SERVICE_NAME) { - if (this._fingerprintFailedId) - GLib.source_remove(this._fingerprintFailedId); -- -- // On Fedora we have the problem that fingerprint auth fails -- // immediately if the PAM configuration has not been updated and no -- // prints are enrolled. -- // So, consider a verification failure within one second to be a service -- // failure instead. -- if (this._fprintStartTime > GLib.get_monotonic_time() - GLib.USEC_PER_SEC) { -- log("Fingerprint service failed almost immediately, considering it unavailable."); -- log("Please fix your configuration by running: authselect select --force sssd with-fingerprint with-silent-lastlog"); -- this._onServiceUnavailable(this._client, serviceName, null); -- return; -- } - } - - // For Not Listed / enterprise logins, immediately reset @@ -5933,7 +6583,7 @@ index 6c9e0a54a2..4c95fd0b6f 100644 } } -@@ -859,47 +455,4 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -840,47 +457,4 @@ export class ShellUserVerifier extends Signals.EventEmitter { }); }); } @@ -5982,7 +6632,7 @@ index 6c9e0a54a2..4c95fd0b6f 100644 - } } diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml -index 11de814fbd..e7020fe013 100644 +index 26b55b75bb..b6801acdb7 100644 --- a/js/js-resources.gresource.xml +++ b/js/js-resources.gresource.xml @@ -4,6 +4,8 @@ @@ -5995,11 +6645,11 @@ index 11de814fbd..e7020fe013 100644 gdm/const.js gdm/credentialManager.js diff --git a/js/misc/fingerprintManager.js b/js/misc/fingerprintManager.js -index 60b5163b48..cddc9f5425 100644 +index 5bcb7bf6f0..609017a571 100644 --- a/js/misc/fingerprintManager.js +++ b/js/misc/fingerprintManager.js -@@ -44,10 +44,6 @@ export class FingerprintManager extends Signals.EventEmitter { - return this._fingerprintReaderFound; +@@ -67,10 +67,6 @@ class FingerprintManager extends GObject.Object { + return this.readerType !== FingerprintReaderType.NONE; } - setDefaultTimeout(timeout) { @@ -6010,12 +6660,12 @@ index 60b5163b48..cddc9f5425 100644 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 8be425615d..782fed0d11 100644 +index ee0829c96e..eb58487b4c 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in -@@ -12,6 +12,7 @@ js/dbusServices/extensions/extensionPrefsDialog.js +@@ -11,6 +11,7 @@ data/X-GNOME-Shell-Utilities.directory.desktop.in + js/dbusServices/extensions/extensionPrefsDialog.js js/dbusServices/extensions/ui/extension-error-page.ui - js/gdm/authList.js js/gdm/authPrompt.js +js/gdm/authServicesLegacy.js js/gdm/loginDialog.js @@ -6025,10 +6675,142 @@ index 8be425615d..782fed0d11 100644 2.53.0 -From 682906910fb06886e6d8af6dcf8707f93189a4d3 Mon Sep 17 00:00:00 2001 +From c2118d91c727bea67722313839d72db170993659 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 + +Fingerprint mechanism now has a ready state that controls when it +appears in the authentication UI. When fingerprint authentication +starts, the mechanism is marked as not ready and filtered from the +enabledMechanisms list. Only after the fingerprint service has been +running for a brief window (0.5s) is it marked as ready and emitted +via mechanisms-changed. + +This prevents the fingerprint icon from briefly appearing in the UI +for users who don't have enrolled fingerprints. In those cases, the +fingerprint service starts but quickly stops with service-unavailable, +so the timeout never completes and the icon never shows. +--- + js/gdm/authServices.js | 2 +- + js/gdm/authServicesLegacy.js | 55 ++++++++++++++++++++++++++++++++---- + 2 files changed, 51 insertions(+), 6 deletions(-) + +diff --git a/js/gdm/authServices.js b/js/gdm/authServices.js +index f702b15a04..7f0cda791a 100644 +--- a/js/gdm/authServices.js ++++ b/js/gdm/authServices.js +@@ -87,7 +87,7 @@ export class AuthServices extends GObject.Object { + } + + get enabledMechanisms() { +- return this._enabledMechanisms; ++ return this._enabledMechanisms?.filter(m => m.ready !== false); + } + + get _roleToService() { +diff --git a/js/gdm/authServicesLegacy.js b/js/gdm/authServicesLegacy.js +index e9c1676ab9..68a3c6990a 100644 +--- a/js/gdm/authServicesLegacy.js ++++ b/js/gdm/authServicesLegacy.js +@@ -9,6 +9,7 @@ import * as Vmware from './vmware.js'; + import {AuthServices} from './authServices.js'; + + const FINGERPRINT_ERROR_TIMEOUT_WAIT = 15; ++const FINGERPRINT_READY_TIMEOUT_MS = 500; + + const Mechanisms = [ + { +@@ -55,6 +56,8 @@ export class AuthServicesLegacy extends AuthServices { + this._credentialManagers = {}; + 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 { + + _handleClear() { + this._smartcardInProgress = false; ++ this._clearFingerprintReadyTimeout(); ++ } ++ ++ _clearFingerprintReadyTimeout() { ++ if (this._fingerprintReadyTimeoutId) { ++ GLib.source_remove(this._fingerprintReadyTimeoutId); ++ this._fingerprintReadyTimeoutId = 0; ++ } ++ } ++ ++ _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) ++ return; ++ ++ mechanism.ready = true; ++ ++ this.emit('mechanisms-changed'); + } + + _handleUpdateEnabledMechanisms() { +@@ -115,6 +151,13 @@ 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; + } + } + +@@ -232,11 +275,13 @@ export class AuthServicesLegacy extends AuthServices { + return; + } + +- if (serviceName === Const.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) { ++ this._clearFingerprintReadyTimeout(); ++ if (this._unavailableServices.has(serviceName)) { ++ this._enabledMechanisms = this._enabledMechanisms ++ .filter(m => m.serviceName !== serviceName); ++ this.emit('mechanisms-changed'); ++ } + } + + if (this._unavailableServices.has(serviceName)) +-- +2.53.0 + + +From 0ec2d1f973cc28ab3f3b4bb5476b348627e987ba Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Tue, 6 Feb 2024 14:09:34 -0500 -Subject: [PATCH 24/30] gdm: Add authServicesSwitchable +Subject: [PATCH 36/42] 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' @@ -6045,14 +6827,14 @@ The basic workflow: that indicates which mechanism should be used first. 3. Those mechanisms are formated and stored in _enabledMechanisms. The default mechanism is selected and the 'mechanisms-changed' signal is emitted. -4. When the mechanism is selected, it uses its role with a switch to - handle the different authentication flows. Depending on the role +4. When the mechanism is selected, its role is used with a switch to + handle the different authentication flows. Depending on the role, different signals will be emitted, like 'ask-question'... -5. When handling user feedback, it uses the selected mechanism role with - a switch to determine how to format the response and sends it using +5. When handling user feedback, the selected mechanism role is used with + a switch to determine how to format the response and is sent using _userVerifierCustomJson. -6. When the verification succeeds, the 'verification-complete' signal - is emitted, if it fails, 'verification-failed' is emitted. +6. When verification succeeds, 'verification-complete' signal is emitted, + if it fails, 'verification-failed' is emitted. Now authServicesLegacy and authServicesSwitchable are mutually exclusive, but in the next commit they won't be. @@ -6062,29 +6844,30 @@ 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/authServicesSwitchable.js | 162 +++++++++++++++++++++++++++++++ - js/gdm/loginDialog.js | 5 +- - js/gdm/util.js | 43 ++++++-- - js/js-resources.gresource.xml | 1 + - js/ui/unlockDialog.js | 1 + - po/POTFILES.in | 1 + - 6 files changed, 205 insertions(+), 8 deletions(-) - create mode 100644 js/gdm/authServicesSwitchable.js + js/gdm/authServicesSSSDSwitchable.js | 167 +++++++++++++++++++++++++++ + js/gdm/loginDialog.js | 5 +- + js/gdm/util.js | 46 ++++++-- + js/js-resources.gresource.xml | 1 + + js/ui/unlockDialog.js | 1 + + po/POTFILES.in | 1 + + 6 files changed, 213 insertions(+), 8 deletions(-) + create mode 100644 js/gdm/authServicesSSSDSwitchable.js -diff --git a/js/gdm/authServicesSwitchable.js b/js/gdm/authServicesSwitchable.js +diff --git a/js/gdm/authServicesSSSDSwitchable.js b/js/gdm/authServicesSSSDSwitchable.js new file mode 100644 -index 0000000000..4913cb2f41 +index 0000000000..b6e2e06697 --- /dev/null -+++ b/js/gdm/authServicesSwitchable.js -@@ -0,0 +1,162 @@ ++++ b/js/gdm/authServicesSSSDSwitchable.js +@@ -0,0 +1,167 @@ +import GObject from 'gi://GObject'; + +import * as Const from './const.js'; +import * as Util from './util.js'; +import {AuthServices} from './authServices.js'; + -+export const AuthServicesSwitchable = GObject.registerClass({ -+}, class AuthServicesSwitchable extends AuthServices { ++export class AuthServicesSSSDSwitchable extends AuthServices { ++ static [GObject.GTypeName] = 'AuthServicesSSSDSwitchable'; ++ + static SupportedRoles = [ + Const.PASSWORD_ROLE_NAME, + ]; @@ -6093,8 +6876,12 @@ index 0000000000..4913cb2f41 + [Const.PASSWORD_ROLE_NAME]: Const.SWITCHABLE_AUTH_SERVICE_NAME, + }; + -+ _init(params) { -+ super._init(params); ++ static { ++ GObject.registerClass(this); ++ } ++ ++ constructor(params) { ++ super(params); + } + + _handleAnswerQuery(serviceName, answer) { @@ -6238,12 +7025,12 @@ index 0000000000..4913cb2f41 + + this.emit('ask-question', serviceName, prompt, true); + } -+}); ++} diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js -index 70c461f3a6..c7bed1afaf 100644 +index ddb7e603b2..4c08f0957e 100644 --- a/js/gdm/loginDialog.js +++ b/js/gdm/loginDialog.js -@@ -451,7 +451,10 @@ export const LoginDialog = GObject.registerClass({ +@@ -452,7 +452,10 @@ export const LoginDialog = GObject.registerClass({ this._gdmClient = new Gdm.Client(); try { @@ -6256,14 +7043,14 @@ index 70c461f3a6..c7bed1afaf 100644 } diff --git a/js/gdm/util.js b/js/gdm/util.js -index 4c95fd0b6f..6ddd16ef6f 100644 +index 8c11c63c49..ab4eccec42 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'; import * as Params from '../misc/params.js'; import {AuthServicesLegacy} from './authServicesLegacy.js'; -+import {AuthServicesSwitchable} from './authServicesSwitchable.js'; ++import {AuthServicesSSSDSwitchable} from './authServicesSSSDSwitchable.js'; const CLONE_FADE_ANIMATION_TIME = 250; @@ -6275,64 +7062,74 @@ index 4c95fd0b6f..6ddd16ef6f 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'; -@@ -148,6 +150,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -150,6 +152,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { try { - const proxies = await this._getUserVerifierProxies(userName); -+ await this._authServicesSwitchable?.beginVerification(userName, proxies); + const proxies = await this._getUserVerifierProxies(userName, this._cancellable); ++ await this._authServicesSSSDSwitchable?.beginVerification(userName, proxies); await this._authServicesLegacy?.beginVerification(userName, proxies); this._userVerifier = proxies.userVerifier; } catch (e) { -@@ -159,14 +162,17 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -161,14 +164,19 @@ export class ShellUserVerifier extends Signals.EventEmitter { } selectMechanism(mechanism) { - return this._authServicesLegacy?.selectMechanism(mechanism); -+ return this._authServicesSwitchable?.selectMechanism(mechanism) || -+ this._authServicesLegacy?.selectMechanism(mechanism); ++ let selected = false; ++ selected |= this._authServicesSSSDSwitchable?.selectMechanism(mechanism); ++ selected |= this._authServicesLegacy?.selectMechanism(mechanism); ++ return selected; } needsUsername() { - return this._authServicesLegacy?.needsUsername(); -+ return this._authServicesSwitchable?.needsUsername() || ++ return this._authServicesSSSDSwitchable?.needsUsername() || + this._authServicesLegacy?.needsUsername(); } reset() { -+ this._authServicesSwitchable?.reset(); ++ this._authServicesSSSDSwitchable?.reset(); this._authServicesLegacy?.reset(); this._userVerifier?.call_cancel_sync(null); -@@ -175,6 +181,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -177,6 +185,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { } cancel() { -+ this._authServicesSwitchable?.cancel(); ++ this._authServicesSSSDSwitchable?.cancel(); this._authServicesLegacy?.cancel(); this._userVerifier?.call_cancel_sync(null); -@@ -183,6 +190,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -185,6 +194,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { } clear() { -+ this._authServicesSwitchable?.clear(); ++ this._authServicesSSSDSwitchable?.clear(); this._authServicesLegacy?.clear(); this._clearMessageQueue(); -@@ -201,10 +209,12 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -196,6 +206,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { + } + + destroy() { ++ this._authServicesSSSDSwitchable?.disconnectObject(this); + this._authServicesLegacy?.disconnectObject(this); + + this.cancel(); +@@ -205,10 +216,12 @@ export class ShellUserVerifier extends Signals.EventEmitter { } selectChoice(serviceName, key) { -+ this._authServicesSwitchable?.selectChoice(serviceName, key); ++ this._authServicesSSSDSwitchable?.selectChoice(serviceName, key); this._authServicesLegacy?.selectChoice(serviceName, key); } answerQuery(serviceName, answer) { -+ this._authServicesSwitchable?.answerQuery(serviceName, answer); ++ this._authServicesSSSDSwitchable?.answerQuery(serviceName, answer); this._authServicesLegacy?.answerQuery(serviceName, answer); } -@@ -370,10 +380,15 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -372,10 +385,15 @@ export class ShellUserVerifier extends Signals.EventEmitter { if (this._settings.get_boolean(FINGERPRINT_AUTHENTICATION_KEY)) enabledRoles.push(Const.FINGERPRINT_ROLE_NAME); @@ -6349,14 +7146,14 @@ index 4c95fd0b6f..6ddd16ef6f 100644 this._createAuthServices(); } -@@ -387,20 +402,30 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -389,20 +407,30 @@ export class ShellUserVerifier extends Signals.EventEmitter { allowedFailures: this.allowedFailures, reauthOnly: this._reauthOnly, }; - if (AuthServicesLegacy.supportsAny(this._enabledRoles)) + if (this._switchableAuthenticationEnabled && -+ AuthServicesSwitchable.supportsAny(this._enabledRoles)) -+ this._authServicesSwitchable = new AuthServicesSwitchable(params); ++ AuthServicesSSSDSwitchable.supportsAny(this._enabledRoles)) ++ this._authServicesSSSDSwitchable = new AuthServicesSSSDSwitchable(params); + else if (AuthServicesLegacy.supportsAny(this._enabledRoles)) this._authServicesLegacy = new AuthServicesLegacy(params); @@ -6364,9 +7161,9 @@ index 4c95fd0b6f..6ddd16ef6f 100644 } _clearAuthServices() { -+ this._authServicesSwitchable?.disconnectObject(this); -+ this._authServicesSwitchable?.clear(); -+ this._authServicesSwitchable = null; ++ this._authServicesSSSDSwitchable?.disconnectObject(this); ++ this._authServicesSSSDSwitchable?.clear(); ++ this._authServicesSSSDSwitchable = null; + this._authServicesLegacy?.disconnectObject(this); this._authServicesLegacy?.clear(); @@ -6376,45 +7173,45 @@ index 4c95fd0b6f..6ddd16ef6f 100644 _connectAuthServices() { - [this._authServicesLegacy].forEach(authServices => { + [ -+ this._authServicesSwitchable, ++ this._authServicesSSSDSwitchable, + this._authServicesLegacy, + ].forEach(authServices => { authServices?.connectObject( 'ask-question', (_, ...args) => this.emit('ask-question', ...args), 'queue-message', (_, ...args) => this._queueMessage(...args), -@@ -422,9 +447,13 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -424,9 +452,13 @@ export class ShellUserVerifier extends Signals.EventEmitter { } _onMechanismsChanged() { - const mechanisms = this._authServicesLegacy?.enabledMechanisms ?? []; -+ const mechanismsSwitchable = this._authServicesSwitchable?.enabledMechanisms ?? []; ++ const mechanismsSwitchable = this._authServicesSSSDSwitchable?.enabledMechanisms ?? []; + const mechanismsLegacy = this._authServicesLegacy?.enabledMechanisms ?? []; + const mechanisms = [...mechanismsSwitchable, ...mechanismsLegacy]; - const selectedMechanism = this._authServicesLegacy?.selectedMechanism ?? + const selectedMechanism = -+ this._authServicesSwitchable?.selectedMechanism ?? ++ this._authServicesSSSDSwitchable?.selectedMechanism ?? + this._authServicesLegacy?.selectedMechanism ?? mechanisms.find(m => isSelectable(m)) ?? {}; diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml -index e7020fe013..ca4856e565 100644 +index b6801acdb7..a7bcbd8898 100644 --- a/js/js-resources.gresource.xml +++ b/js/js-resources.gresource.xml @@ -6,6 +6,7 @@ gdm/authPrompt.js gdm/authServices.js gdm/authServicesLegacy.js -+ gdm/authServicesSwitchable.js ++ gdm/authServicesSSSDSwitchable.js gdm/batch.js gdm/const.js gdm/credentialManager.js diff --git a/js/ui/unlockDialog.js b/js/ui/unlockDialog.js -index 5deb435daa..90130269a9 100644 +index 80c3dd813a..a16f68fee3 100644 --- a/js/ui/unlockDialog.js +++ b/js/ui/unlockDialog.js -@@ -568,6 +568,7 @@ export const UnlockDialog = GObject.registerClass({ +@@ -570,6 +570,7 @@ export const UnlockDialog = GObject.registerClass({ try { this._gdmClient.set_enabled_extensions([ Gdm.UserVerifierChoiceList.interface_info().name, @@ -6423,14 +7220,14 @@ index 5deb435daa..90130269a9 100644 } catch { } diff --git a/po/POTFILES.in b/po/POTFILES.in -index 782fed0d11..6c4ed8012e 100644 +index eb58487b4c..d1e4d64872 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in -@@ -13,6 +13,7 @@ js/dbusServices/extensions/ui/extension-error-page.ui - js/gdm/authList.js +@@ -12,6 +12,7 @@ js/dbusServices/extensions/extensionPrefsDialog.js + js/dbusServices/extensions/ui/extension-error-page.ui js/gdm/authPrompt.js js/gdm/authServicesLegacy.js -+js/gdm/authServicesSwitchable.js ++js/gdm/authServicesSSSDSwitchable.js js/gdm/loginDialog.js js/gdm/util.js js/misc/breakManager.js @@ -6438,10 +7235,10 @@ index 782fed0d11..6c4ed8012e 100644 2.53.0 -From 1c88e0f7cb77668dd0104ca1760fb8d89d2cdd3e Mon Sep 17 00:00:00 2001 +From baa6d1f833d409208661e69e5474158e94e8f089 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Wed, 12 Nov 2025 17:25:33 +0100 -Subject: [PATCH 25/30] authServicesSwitchable: Allow resetting expired +Subject: [PATCH 37/42] authServicesSSSDSwitchable: Allow resetting expired password JSON protocol can't inform when a password is expired, so it's needed to @@ -6452,15 +7249,15 @@ 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/authServicesSwitchable.js | 27 ++++++++++++++++++++++++++- + js/gdm/authServicesSSSDSwitchable.js | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) -diff --git a/js/gdm/authServicesSwitchable.js b/js/gdm/authServicesSwitchable.js -index 4913cb2f41..48a0be4433 100644 ---- a/js/gdm/authServicesSwitchable.js -+++ b/js/gdm/authServicesSwitchable.js -@@ -18,10 +18,19 @@ export const AuthServicesSwitchable = GObject.registerClass({ - super._init(params); +diff --git a/js/gdm/authServicesSSSDSwitchable.js b/js/gdm/authServicesSSSDSwitchable.js +index b6e2e06697..5f89478ff9 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) { @@ -6480,7 +7277,7 @@ index 4913cb2f41..48a0be4433 100644 let response; switch (this._selectedMechanism.role) { case Const.PASSWORD_ROLE_NAME: -@@ -53,6 +62,8 @@ export const AuthServicesSwitchable = GObject.registerClass({ +@@ -58,6 +67,8 @@ export class AuthServicesSSSDSwitchable extends AuthServices { this._priorityList = null; this._enabledMechanisms = null; this._selectedMechanism = null; @@ -6489,7 +7286,7 @@ index 4913cb2f41..48a0be4433 100644 } _handleOnCustomJSONRequest(_serviceName, _protocol, _version, json) { -@@ -97,6 +108,13 @@ export const AuthServicesSwitchable = GObject.registerClass({ +@@ -102,6 +113,13 @@ export class AuthServicesSSSDSwitchable extends AuthServices { } _handleOnInfo(serviceName, info) { @@ -6503,7 +7300,7 @@ index 4913cb2f41..48a0be4433 100644 if (serviceName === this._selectedMechanism?.serviceName) this.emit('queue-message', serviceName, info, Util.MessageType.INFO); } -@@ -110,6 +128,13 @@ export const AuthServicesSwitchable = GObject.registerClass({ +@@ -115,6 +133,13 @@ export class AuthServicesSSSDSwitchable extends AuthServices { } } @@ -6521,37 +7318,37 @@ index 4913cb2f41..48a0be4433 100644 2.53.0 -From 2945dc7c87a2be9fdf89af9e8ba730f0e394d4d7 Mon Sep 17 00:00:00 2001 +From 901bb24c5228ab67f417abfe32c1ffde066be986 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Thu, 21 Aug 2025 22:25:02 +0200 -Subject: [PATCH 26/30] gdm: Allow starting authServicesLegacy as fallback +Subject: [PATCH 38/42] gdm: Allow starting authServicesLegacy as fallback -There can be cases where authServicesSwitchable doesn't support some +There can be cases where authServicesSSSDSwitchable doesn't support some authentication methods. In such cases, we should start authServicesLegacy as a fallback for those unsupported methods. -authServicesSwitchable leads when the fallback will happen emitting -'mechanisms-changed'. Then unsupportedRoles, from authServicesSwitchable will +authServicesSSSDSwitchable leads when the fallback will happen emitting +'mechanisms-changed'. Then unsupportedRoles, from authServicesSSSDSwitchable will be used to update the enabledRoles in authServicesLegacy. -While authServicesSwitchable hasn't started its service and didn't receive +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). --- - js/gdm/authServices.js | 22 ++++++++++++ - js/gdm/authServicesLegacy.js | 5 +++ - js/gdm/authServicesSwitchable.js | 62 ++++++++++++++++++++++++++++++-- - js/gdm/util.js | 24 +++++++++++-- - 4 files changed, 109 insertions(+), 4 deletions(-) + 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(-) diff --git a/js/gdm/authServices.js b/js/gdm/authServices.js -index 68a2c4c3f5..75fc0681d9 100644 +index 7f0cda791a..e2dbba8aff 100644 --- a/js/gdm/authServices.js +++ b/js/gdm/authServices.js -@@ -91,6 +91,10 @@ export const AuthServices = GObject.registerClass({ +@@ -98,6 +98,10 @@ export class AuthServices extends GObject.Object { return this.constructor.SupportedRoles; } @@ -6562,7 +7359,7 @@ index 68a2c4c3f5..75fc0681d9 100644 selectChoice(serviceName, key) { this._handleSelectChoice(serviceName, key); } -@@ -166,6 +170,18 @@ export const AuthServices = GObject.registerClass({ +@@ -173,6 +177,18 @@ export class AuthServices extends GObject.Object { this._handleClear(); } @@ -6581,7 +7378,7 @@ index 68a2c4c3f5..75fc0681d9 100644 _clearUserVerifier() { this._disconnectUserVerifierSignals(); this._userVerifier = null; -@@ -409,6 +425,10 @@ export const AuthServices = GObject.registerClass({ +@@ -416,6 +432,10 @@ export class AuthServices extends GObject.Object { } } @@ -6592,21 +7389,21 @@ index 68a2c4c3f5..75fc0681d9 100644 _handleSelectChoice() {} async _handleAnswerQuery() {} -@@ -427,6 +447,8 @@ export const AuthServices = GObject.registerClass({ +@@ -434,6 +454,8 @@ export class AuthServices extends GObject.Object { _handleClear() {} + _handleUpdateEnabledRoles() {} + - _handleUpdateEnabledMechanisms() {} - - _handleSmartcardChanged() {} + _handleUpdateEnabledMechanisms() { + throw new GObject.NotImplementedError( + `_handleUpdateEnabledMechanisms in ${this.constructor.name}`); diff --git a/js/gdm/authServicesLegacy.js b/js/gdm/authServicesLegacy.js -index 93f55ec45d..594ffc09a0 100644 +index 68a3c6990a..0813622a01 100644 --- a/js/gdm/authServicesLegacy.js +++ b/js/gdm/authServicesLegacy.js -@@ -100,6 +100,11 @@ export const AuthServicesLegacy = GObject.registerClass({ - this._smartcardInProgress = false; +@@ -141,6 +141,11 @@ export class AuthServicesLegacy extends AuthServices { + this.emit('mechanisms-changed'); } + _handleUpdateEnabledRoles() { @@ -6617,10 +7414,10 @@ index 93f55ec45d..594ffc09a0 100644 _handleUpdateEnabledMechanisms() { if (!this._fingerprintManager?.readerFound) { this._enabledMechanisms.push(...Mechanisms.filter(m => -diff --git a/js/gdm/authServicesSwitchable.js b/js/gdm/authServicesSwitchable.js -index 48a0be4433..e2be3d3133 100644 ---- a/js/gdm/authServicesSwitchable.js -+++ b/js/gdm/authServicesSwitchable.js +diff --git a/js/gdm/authServicesSSSDSwitchable.js b/js/gdm/authServicesSSSDSwitchable.js +index 5f89478ff9..9649dd1382 100644 +--- a/js/gdm/authServicesSSSDSwitchable.js ++++ b/js/gdm/authServicesSSSDSwitchable.js @@ -4,6 +4,12 @@ import * as Const from './const.js'; import * as Util from './util.js'; import {AuthServices} from './authServices.js'; @@ -6631,19 +7428,19 @@ index 48a0be4433..e2be3d3133 100644 + FOUND: 2, +}; + - export const AuthServicesSwitchable = GObject.registerClass({ - }, class AuthServicesSwitchable extends AuthServices { - static SupportedRoles = [ -@@ -16,6 +22,8 @@ export const AuthServicesSwitchable = GObject.registerClass({ + export class AuthServicesSSSDSwitchable extends AuthServices { + static [GObject.GTypeName] = 'AuthServicesSSSDSwitchable'; - _init(params) { - super._init(params); +@@ -21,6 +27,8 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + + constructor(params) { + super(params); + + this._mechanismsStatus = MechanismsStatus.WAITING; } async _handleAnswerQuery(serviceName, answer) { -@@ -48,13 +56,30 @@ export const AuthServicesSwitchable = GObject.registerClass({ +@@ -53,13 +61,30 @@ export class AuthServicesSSSDSwitchable extends AuthServices { } } @@ -6675,7 +7472,7 @@ index 48a0be4433..e2be3d3133 100644 } _handleClear() { -@@ -84,6 +109,10 @@ export const AuthServicesSwitchable = GObject.registerClass({ +@@ -89,6 +114,10 @@ export class AuthServicesSSSDSwitchable extends AuthServices { if (this._mechanisms) this._updateEnabledMechanisms(); } @@ -6686,7 +7483,7 @@ index 48a0be4433..e2be3d3133 100644 } _handleUpdateEnabledMechanisms() { -@@ -108,6 +137,9 @@ export const AuthServicesSwitchable = GObject.registerClass({ +@@ -113,6 +142,9 @@ export class AuthServicesSSSDSwitchable extends AuthServices { } _handleOnInfo(serviceName, info) { @@ -6696,7 +7493,7 @@ index 48a0be4433..e2be3d3133 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 && -@@ -120,6 +152,9 @@ export const AuthServicesSwitchable = GObject.registerClass({ +@@ -125,6 +157,9 @@ export class AuthServicesSSSDSwitchable extends AuthServices { } _handleOnProblem(serviceName, problem) { @@ -6706,7 +7503,7 @@ index 48a0be4433..e2be3d3133 100644 if (serviceName === this._selectedMechanism?.serviceName) { this.emit('queue-priority-message', serviceName, -@@ -128,7 +163,16 @@ export const AuthServicesSwitchable = GObject.registerClass({ +@@ -133,7 +168,16 @@ export class AuthServicesSSSDSwitchable extends AuthServices { } } @@ -6723,7 +7520,7 @@ index 48a0be4433..e2be3d3133 100644 if (serviceName === this._selectedMechanism?.serviceName && this._selectedMechanism.role === Const.PASSWORD_ROLE_NAME && this._resettingPassword) -@@ -148,7 +192,7 @@ export const AuthServicesSwitchable = GObject.registerClass({ +@@ -153,7 +197,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { _handleCanStartService(serviceName) { return serviceName === Const.SWITCHABLE_AUTH_SERVICE_NAME && @@ -6732,7 +7529,7 @@ index 48a0be4433..e2be3d3133 100644 } _formatResponse(answer) { -@@ -179,6 +223,20 @@ export const AuthServicesSwitchable = GObject.registerClass({ +@@ -184,6 +228,20 @@ export class AuthServicesSSSDSwitchable extends AuthServices { serviceName, JSON.stringify(response), this._cancellable, null); } @@ -6754,31 +7551,31 @@ index 48a0be4433..e2be3d3133 100644 const {serviceName, prompt} = this._selectedMechanism; diff --git a/js/gdm/util.js b/js/gdm/util.js -index 6ddd16ef6f..a18f61c64b 100644 +index ab4eccec42..36a55822cc 100644 --- a/js/gdm/util.js +++ b/js/gdm/util.js -@@ -193,6 +193,11 @@ export class ShellUserVerifier extends Signals.EventEmitter { - this._authServicesSwitchable?.clear(); +@@ -197,6 +197,11 @@ export class ShellUserVerifier extends Signals.EventEmitter { + this._authServicesSSSDSwitchable?.clear(); this._authServicesLegacy?.clear(); -+ if (this._authServicesSwitchable) { ++ if (this._authServicesSSSDSwitchable) { + this._authServicesLegacy?.updateEnabledRoles( -+ this._authServicesSwitchable.unsupportedRoles); ++ this._authServicesSSSDSwitchable.unsupportedRoles); + } + this._clearMessageQueue(); this._cancellable?.cancel(); -@@ -403,10 +408,14 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -408,10 +413,14 @@ export class ShellUserVerifier extends Signals.EventEmitter { reauthOnly: this._reauthOnly, }; if (this._switchableAuthenticationEnabled && -- AuthServicesSwitchable.supportsAny(this._enabledRoles)) -+ AuthServicesSwitchable.supportsAny(this._enabledRoles)) { - this._authServicesSwitchable = new AuthServicesSwitchable(params); +- AuthServicesSSSDSwitchable.supportsAny(this._enabledRoles)) ++ AuthServicesSSSDSwitchable.supportsAny(this._enabledRoles)) { + this._authServicesSSSDSwitchable = new AuthServicesSSSDSwitchable(params); - else if (AuthServicesLegacy.supportsAny(this._enabledRoles)) + -+ params.enabledRoles = this._authServicesSwitchable.unsupportedRoles; ++ params.enabledRoles = this._authServicesSSSDSwitchable.unsupportedRoles; this._authServicesLegacy = new AuthServicesLegacy(params); + } else if (AuthServicesLegacy.supportsAny(this._enabledRoles)) { + this._authServicesLegacy = new AuthServicesLegacy(params); @@ -6786,26 +7583,31 @@ index 6ddd16ef6f..a18f61c64b 100644 this._connectAuthServices(); } -@@ -447,6 +456,9 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -452,9 +461,12 @@ export class ShellUserVerifier extends Signals.EventEmitter { } _onMechanismsChanged() { +- const mechanismsSwitchable = this._authServicesSSSDSwitchable?.enabledMechanisms ?? []; + if (this._enableFallbackMechanisms()) + return; + - const mechanismsSwitchable = this._authServicesSwitchable?.enabledMechanisms ?? []; ++ const mechanismsSSSDSwitchable = this._authServicesSSSDSwitchable?.enabledMechanisms ?? []; const mechanismsLegacy = this._authServicesLegacy?.enabledMechanisms ?? []; - const mechanisms = [...mechanismsSwitchable, ...mechanismsLegacy]; -@@ -460,6 +472,14 @@ export class ShellUserVerifier extends Signals.EventEmitter { +- const mechanisms = [...mechanismsSwitchable, ...mechanismsLegacy]; ++ const mechanisms = [...mechanismsSSSDSwitchable, ...mechanismsLegacy]; + + const selectedMechanism = + this._authServicesSSSDSwitchable?.selectedMechanism ?? +@@ -465,6 +477,14 @@ export class ShellUserVerifier extends Signals.EventEmitter { this.emit('mechanisms-changed', mechanisms, selectedMechanism); } + _enableFallbackMechanisms() { -+ if (!this._authServicesSwitchable || !this._authServicesLegacy) ++ if (!this._authServicesSSSDSwitchable || !this._authServicesLegacy) + return false; + + return this._authServicesLegacy.updateEnabledRoles( -+ this._authServicesSwitchable.unsupportedRoles); ++ this._authServicesSSSDSwitchable.unsupportedRoles); + } + async _waitPendingMessages(waiter) { @@ -6815,11 +7617,234 @@ index 6ddd16ef6f..a18f61c64b 100644 2.53.0 -From eb15295260ad5c9e84b450f3184ff4e041629ba4 Mon Sep 17 00:00:00 2001 +From e3e3917d895f852c286bde7030365f25c6c024a2 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 + +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 +the space available. + +The qr code is always generated using the minimum sized image with a +transparent background (so that the widget background color is used) +while the foreground is colored following the theme. + +The texture is then draw using the nearest filter so that it will adjust +to the actor size without the need to regenerate it on size changes +theme +--- + data/theme/gnome-shell-sass/_widgets.scss | 1 + + .../gnome-shell-sass/widgets/_qr-code.scss | 14 +++ + data/theme/meson.build | 1 + + js/js-resources.gresource.xml | 1 + + js/misc/dependencies.js | 1 + + js/ui/qrCode.js | 119 ++++++++++++++++++ + 6 files changed, 137 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 +--- a/data/theme/gnome-shell-sass/_widgets.scss ++++ b/data/theme/gnome-shell-sass/_widgets.scss +@@ -45,5 +45,6 @@ + @import 'widgets/misc'; + @import 'widgets/keyboard'; + @import 'widgets/looking-glass'; ++@import 'widgets/qr-code'; + // Lock / login screen + @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 +--- /dev/null ++++ b/data/theme/gnome-shell-sass/widgets/_qr-code.scss +@@ -0,0 +1,14 @@ ++// 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; ++ } @else { ++ background-color: $system_fg_color; ++ border-color: $system_fg_color; ++ color: $system_bg_color; ++ } ++} +diff --git a/data/theme/meson.build b/data/theme/meson.build +index 8d01ae826d..d6f2d8e802 100644 +--- a/data/theme/meson.build ++++ b/data/theme/meson.build +@@ -28,6 +28,7 @@ theme_sources = files([ + 'gnome-shell-sass/widgets/_overview.scss', + 'gnome-shell-sass/widgets/_panel.scss', + 'gnome-shell-sass/widgets/_popovers.scss', ++ 'gnome-shell-sass/widgets/_qr-code.scss', + 'gnome-shell-sass/widgets/_quick-settings.scss', + '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 +--- a/js/js-resources.gresource.xml ++++ b/js/js-resources.gresource.xml +@@ -108,6 +108,7 @@ + ui/pointerWatcher.js + ui/popupMenu.js + ui/quickSettings.js ++ ui/qrCode.js + ui/remoteSearch.js + ui/ripples.js + ui/runDialog.js +diff --git a/js/misc/dependencies.js b/js/misc/dependencies.js +index c3c51aa2de..fbb83d1164 100644 +--- a/js/misc/dependencies.js ++++ b/js/misc/dependencies.js +@@ -16,6 +16,7 @@ import 'gi://GioUnix?version=2.0'; + import 'gi://GDesktopEnums?version=3.0'; + import 'gi://GdkPixbuf?version=2.0'; + import 'gi://GnomeBG?version=4.0'; ++import 'gi://GnomeQR?version=4.0'; + import 'gi://GnomeDesktop?version=4.0'; + import 'gi://Graphene?version=1.0'; + 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 +--- /dev/null ++++ b/js/ui/qrCode.js +@@ -0,0 +1,119 @@ ++import Clutter from 'gi://Clutter'; ++import Cogl from 'gi://Cogl'; ++import GObject from 'gi://GObject'; ++import Gio from 'gi://Gio'; ++import GnomeQR from 'gi://GnomeQR'; ++import St from 'gi://St'; ++import {logErrorUnlessCancelled} from '../misc/errorUtils.js'; ++ ++Gio._promisify(GnomeQR, 'generate_qr_code_async'); ++ ++const QR_CODE_DEFAULT_SIZE = 150; ++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, ++ GObject.ParamFlags.READWRITE, ++ null), ++ }; ++ ++ static { ++ GObject.registerClass(this); ++ } ++ ++ constructor(params) { ++ const qrSize = Math.max(params.width ?? 0, params.height ?? 0) || ++ QR_CODE_DEFAULT_SIZE; ++ ++ super({ ++ styleClass: 'qr-code', ++ width: qrSize, ++ height: qrSize, ++ ...params, ++ }); ++ ++ this.set_child(new Clutter.Actor({ ++ xExpand: true, ++ yExpand: true, ++ xAlign: Clutter.ActorAlign.FILL, ++ yAlign: Clutter.ActorAlign.FILL, ++ minificationFilter: Clutter.ScalingFilter.NEAREST, ++ magnificationFilter: Clutter.ScalingFilter.NEAREST, ++ })); ++ ++ this._fgColor = null; ++ ++ this.connect('notify::url', () => ++ this._update().catch(logErrorUnlessCancelled)); ++ }; ++ ++ vfunc_allocate(box) { ++ const width = box.get_width(); ++ const height = box.get_height(); ++ ++ box.set_size(Math.min(width, height), Math.min(width, height)); ++ super.vfunc_allocate(box); ++ } ++ ++ vfunc_style_changed() { ++ super.vfunc_style_changed(); ++ ++ if (!this.get_parent()) ++ return; ++ ++ const node = this.get_theme_node(); ++ const fgColor = node.get_foreground_color(); ++ ++ if (!this._fgColor?.equal(fgColor)) { ++ this._fgColor = fgColor; ++ this._update().catch(logErrorUnlessCancelled); ++ } ++ } ++ ++ async _update() { ++ const fgColor = QrCode.#getGnomeQRColor(this._fgColor); ++ ++ this._cancellable?.cancel(); ++ const cancellable = new Gio.Cancellable(); ++ this._cancellable = cancellable; ++ ++ const [pixelData, qrSize] = await GnomeQR.generate_qr_code_async( ++ this.url, ++ 0 /* size, are fine with the minimum value */, ++ QR_CODE_TRANSPARENT_COLOR, ++ fgColor, ++ GnomeQR.PixelFormat.RGBA_8888, ++ GnomeQR.EccLevel.LOW, ++ cancellable); ++ ++ const coglContext = global.stage.context.get_backend().get_cogl_context(); ++ const bpp = Cogl.pixel_format_get_bytes_per_pixel(Cogl.PixelFormat.RGBA_8888, 0); ++ const rowStride = qrSize * bpp; ++ ++ const content = St.ImageContent.new_with_preferred_size(qrSize, qrSize); ++ content.set_bytes( ++ coglContext, ++ pixelData, ++ Cogl.PixelFormat.RGBA_8888, ++ qrSize, ++ qrSize, ++ rowStride); ++ ++ this.child.set_content(content); ++ } ++ ++ static #getGnomeQRColor(color) { ++ if (!color) ++ return null; ++ ++ return new GnomeQR.Color({ ++ red: color.red, ++ green: color.green, ++ blue: color.blue, ++ alpha: color.alpha, ++ }); ++ } ++}; +-- +2.53.0 + + +From d056723159604b6c251e273972599250bedc3897 Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Tue, 6 Feb 2024 14:18:24 -0500 -Subject: [PATCH 27/30] gdm: Add support for Web Login in - authServicesSwitchable +Subject: [PATCH 40/42] gdm: Add support for Web Login in + authServicesSSSDSwitchable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @@ -6834,24 +7859,23 @@ 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 | 106 ++++- + .../gnome-shell-sass/widgets/_login-lock.scss | 100 ++++++ + js/gdm/authPrompt.js | 104 +++++- js/gdm/authServices.js | 6 + - js/gdm/authServicesSwitchable.js | 82 ++++ + js/gdm/authServicesSSSDSwitchable.js | 82 +++++ js/gdm/const.js | 1 + js/gdm/loginDialog.js | 5 +- js/gdm/util.js | 5 + - js/gdm/webLogin.js | 435 ++++++++++++++++++ + js/gdm/webLogin.js | 310 ++++++++++++++++++ js/js-resources.gresource.xml | 1 + - js/misc/dependencies.js | 1 + js/ui/unlockDialog.js | 3 +- js/ui/userWidget.js | 8 + po/POTFILES.in | 1 + - 13 files changed, 742 insertions(+), 12 deletions(-) + 12 files changed, 614 insertions(+), 12 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 58c2ed495c..c591edd940 100644 +index c18a6bb8a8..ec76f01252 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; @@ -6865,7 +7889,7 @@ index 58c2ed495c..c591edd940 100644 } .login-dialog-prompt-entry-area { -@@ -320,8 +324,88 @@ $_gdm_dialog_width: 25em; +@@ -324,8 +328,88 @@ $_gdm_dialog_width: 25em; } } @@ -6954,7 +7978,7 @@ index 58c2ed495c..c591edd940 100644 .unlock-dialog { background-color: transparent; -@@ -479,3 +563,19 @@ $_gdm_dialog_width: 25em; +@@ -483,3 +567,19 @@ $_gdm_dialog_width: 25em; } } } @@ -6975,7 +7999,7 @@ index 58c2ed495c..c591edd940 100644 + } +} diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index 10598e5c08..b2ad9098ce 100644 +index d8c7c6409d..cdad827764 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js @@ -14,6 +14,7 @@ import * as GdmUtil from './util.js'; @@ -6986,15 +8010,15 @@ index 10598e5c08..b2ad9098ce 100644 import {wiggle} from '../misc/animationUtils.js'; const DEFAULT_BUTTON_WELL_ICON_SIZE = 16; -@@ -87,6 +88,7 @@ export const AuthPrompt = GObject.registerClass({ - this._userVerifier.connect('show-message', this._onShowMessage.bind(this)); - this._userVerifier.connect('show-choice-list', this._onShowChoiceList.bind(this)); - this._userVerifier.connect('mechanisms-changed', (_, ...args) => this.emit('mechanisms-changed', ...args)); -+ this._userVerifier.connect('web-login', this._onWebLogin.bind(this)); - this._userVerifier.connect('verification-failed', this._onVerificationFailed.bind(this)); - this._userVerifier.connect('verification-complete', this._onVerificationComplete.bind(this)); - this._userVerifier.connect('reset', this._onReset.bind(this)); -@@ -301,6 +303,28 @@ export const AuthPrompt = GObject.registerClass({ +@@ -88,6 +89,7 @@ export const AuthPrompt = GObject.registerClass({ + 'show-message', this._onShowMessage.bind(this), + 'show-choice-list', this._onShowChoiceList.bind(this), + 'mechanisms-changed', (_, ...args) => this.emit('mechanisms-changed', ...args), ++ 'web-login', this._onWebLogin.bind(this), + 'verification-failed', this._onVerificationFailed.bind(this), + 'verification-complete', this._onVerificationComplete.bind(this), + 'reset', this._onReset.bind(this), +@@ -305,6 +307,28 @@ export const AuthPrompt = GObject.registerClass({ this._defaultButtonWell.add_child(this._spinner); this.setActorInDefaultButtonWell(this._nextButton); @@ -7023,7 +8047,7 @@ index 10598e5c08..b2ad9098ce 100644 } _updateCancelButton() { -@@ -437,6 +461,51 @@ export const AuthPrompt = GObject.registerClass({ +@@ -441,6 +465,51 @@ export const AuthPrompt = GObject.registerClass({ this.emit('prompted'); } @@ -7075,7 +8099,7 @@ index 10598e5c08..b2ad9098ce 100644 _onShowMessage(_userVerifier, serviceName, message, type) { let wiggleParameters = {duration: 0}; -@@ -457,7 +526,9 @@ export const AuthPrompt = GObject.registerClass({ +@@ -461,7 +530,9 @@ export const AuthPrompt = GObject.registerClass({ // show the entry area to allow getting a preemptive answer if (message && !this._entryArea.visible && @@ -7086,7 +8110,7 @@ index 10598e5c08..b2ad9098ce 100644 this._fadeInElement(this._entryArea); this.emit('prompted'); -@@ -483,12 +554,14 @@ export const AuthPrompt = GObject.registerClass({ +@@ -487,12 +558,14 @@ export const AuthPrompt = GObject.registerClass({ this.stopSpinning(true); this.verificationStatus = AuthPromptStatus.VERIFICATION_SUCCEEDED; @@ -7107,7 +8131,7 @@ index 10598e5c08..b2ad9098ce 100644 }); this.emit('verification-complete'); -@@ -560,6 +633,9 @@ export const AuthPrompt = GObject.registerClass({ +@@ -564,6 +637,9 @@ export const AuthPrompt = GObject.registerClass({ stopSpinning(animate) { this.emit('loading', false); this.setActorInDefaultButtonWell(this._nextButton, animate); @@ -7116,8 +8140,8 @@ index 10598e5c08..b2ad9098ce 100644 + this._webLoginDialog.stopLoading(); } - clear() { -@@ -570,10 +646,14 @@ export const AuthPrompt = GObject.registerClass({ + clear(params) { +@@ -581,10 +657,14 @@ export const AuthPrompt = GObject.registerClass({ this._authListTitle.child.text = ''; this._authList.clear(); this._authList.hide(); @@ -7135,7 +8159,7 @@ index 10598e5c08..b2ad9098ce 100644 } setQuestion(question) { -@@ -585,6 +665,8 @@ export const AuthPrompt = GObject.registerClass({ +@@ -596,6 +676,8 @@ export const AuthPrompt = GObject.registerClass({ this._entry.hint_text = question; this._authList.hide(); @@ -7144,22 +8168,20 @@ index 10598e5c08..b2ad9098ce 100644 this._fadeInElement(this._entryArea); } -@@ -680,6 +762,10 @@ export const AuthPrompt = GObject.registerClass({ - - if (this._authList.visible) - authWidget = this._authList; -+ else if (this._webLoginIntro.visible) -+ authWidget = this._webLoginIntro; -+ else if (this._webLoginDialog.visible) -+ authWidget = this._webLoginDialog; - else - authWidget = this._entry; +@@ -689,6 +771,8 @@ export const AuthPrompt = GObject.registerClass({ + updateSensitivity({sensitive}) { + const authWidget = [ + this._authList, ++ this._webLoginIntro, ++ this._webLoginDialog, + ].find(widget => widget.visible) ?? this._entry; + if (authWidget.reactive === sensitive) diff --git a/js/gdm/authServices.js b/js/gdm/authServices.js -index 75fc0681d9..066276b786 100644 +index e2dbba8aff..1801fbdc3a 100644 --- a/js/gdm/authServices.js +++ b/js/gdm/authServices.js -@@ -43,6 +43,12 @@ export const AuthServices = GObject.registerClass({ +@@ -45,6 +45,12 @@ export class AuthServices extends GObject.Object { param_types: [GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_JSOBJECT], }, 'mechanisms-changed': {}, @@ -7169,20 +8191,20 @@ index 75fc0681d9..066276b786 100644 + GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_JSOBJECT, + ], + }, - }, - }, class AuthServices extends GObject.Object { - static SupportedRoles = []; -diff --git a/js/gdm/authServicesSwitchable.js b/js/gdm/authServicesSwitchable.js -index e2be3d3133..d13020e642 100644 ---- a/js/gdm/authServicesSwitchable.js -+++ b/js/gdm/authServicesSwitchable.js + }; + + static { +diff --git a/js/gdm/authServicesSSSDSwitchable.js b/js/gdm/authServicesSSSDSwitchable.js +index 9649dd1382..98171b32c2 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'; -@@ -14,10 +15,12 @@ export const AuthServicesSwitchable = GObject.registerClass({ - }, class AuthServicesSwitchable extends AuthServices { +@@ -15,10 +16,12 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + static SupportedRoles = [ Const.PASSWORD_ROLE_NAME, + Const.WEB_LOGIN_ROLE_NAME, @@ -7193,8 +8215,8 @@ index e2be3d3133..d13020e642 100644 + [Const.WEB_LOGIN_ROLE_NAME]: Const.SWITCHABLE_AUTH_SERVICE_NAME, }; - _init(params) { -@@ -53,6 +56,9 @@ export const AuthServicesSwitchable = GObject.registerClass({ + static { +@@ -58,6 +61,9 @@ export class AuthServicesSSSDSwitchable extends AuthServices { case Const.PASSWORD_ROLE_NAME: this._startPasswordLogin(); break; @@ -7204,7 +8226,7 @@ index e2be3d3133..d13020e642 100644 } } -@@ -89,6 +95,8 @@ export const AuthServicesSwitchable = GObject.registerClass({ +@@ -94,6 +100,8 @@ export class AuthServicesSSSDSwitchable extends AuthServices { this._selectedMechanism = null; this._resettingPassword = false; @@ -7213,7 +8235,7 @@ index e2be3d3133..d13020e642 100644 } _handleOnCustomJSONRequest(_serviceName, _protocol, _version, json) { -@@ -125,6 +133,8 @@ export const AuthServicesSwitchable = GObject.registerClass({ +@@ -130,6 +138,8 @@ export class AuthServicesSSSDSwitchable extends AuthServices { // filter out mechanisms with roles that are not enabled .filter(m => this._enabledRoles.includes(m.role))); @@ -7222,7 +8244,7 @@ index e2be3d3133..d13020e642 100644 const selectedMechanism = this._enabledMechanisms .find(m => this._savedMechanism?.role === m.role) ?? -@@ -136,6 +146,31 @@ export const AuthServicesSwitchable = GObject.registerClass({ +@@ -141,6 +151,31 @@ export class AuthServicesSSSDSwitchable extends AuthServices { this._savedMechanism = null; } @@ -7254,7 +8276,7 @@ index e2be3d3133..d13020e642 100644 _handleOnInfo(serviceName, info) { if (!this._eventExpected()) return; -@@ -204,6 +239,10 @@ export const AuthServicesSwitchable = GObject.registerClass({ +@@ -209,6 +244,10 @@ export class AuthServicesSSSDSwitchable extends AuthServices { response = {password: answer}; break; } @@ -7265,7 +8287,7 @@ index e2be3d3133..d13020e642 100644 default: throw new GObject.NotImplementedError(`formatResponse: ${role}`); } -@@ -242,4 +281,47 @@ export const AuthServicesSwitchable = GObject.registerClass({ +@@ -247,4 +286,47 @@ export class AuthServicesSSSDSwitchable extends AuthServices { this.emit('ask-question', serviceName, prompt, true); } @@ -7312,7 +8334,7 @@ index e2be3d3133..d13020e642 100644 + GLib.source_remove(this._webLoginTimeoutId); + this._webLoginTimeoutId = 0; + } - }); + } diff --git a/js/gdm/const.js b/js/gdm/const.js index 2f37446c8a..e8ca48625a 100644 --- a/js/gdm/const.js @@ -7326,10 +8348,10 @@ 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 c7bed1afaf..13637cea40 100644 +index 4c08f0957e..11b578e38f 100644 --- a/js/gdm/loginDialog.js +++ b/js/gdm/loginDialog.js -@@ -742,7 +742,10 @@ export const LoginDialog = GObject.registerClass({ +@@ -746,7 +746,10 @@ export const LoginDialog = GObject.registerClass({ let authPromptAllocation = null; let authPromptWidth = 0; if (this._authPrompt.visible) { @@ -7342,7 +8364,7 @@ index c7bed1afaf..13637cea40 100644 } diff --git a/js/gdm/util.js b/js/gdm/util.js -index a18f61c64b..be0b77f4ca 100644 +index 36a55822cc..fe2216f8c7 100644 --- a/js/gdm/util.js +++ b/js/gdm/util.js @@ -17,6 +17,7 @@ export const PASSWORD_AUTHENTICATION_KEY = 'enable-password-authentication'; @@ -7353,7 +8375,7 @@ index a18f61c64b..be0b77f4ca 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'; -@@ -93,6 +94,7 @@ export function isSelectable(mechanism) { +@@ -94,6 +95,7 @@ export function isSelectable(mechanism) { switch (mechanism.role) { case Const.PASSWORD_ROLE_NAME: case Const.SMARTCARD_ROLE_NAME: @@ -7361,7 +8383,7 @@ index a18f61c64b..be0b77f4ca 100644 return true; case Const.FINGERPRINT_ROLE_NAME: return false; -@@ -384,6 +386,8 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -389,6 +391,8 @@ export class ShellUserVerifier extends Signals.EventEmitter { enabledRoles.push(Const.SMARTCARD_ROLE_NAME); if (this._settings.get_boolean(FINGERPRINT_AUTHENTICATION_KEY)) enabledRoles.push(Const.FINGERPRINT_ROLE_NAME); @@ -7370,7 +8392,7 @@ index a18f61c64b..be0b77f4ca 100644 const switchableAuthentication = this._settings.get_boolean(SWITCHABLE_AUTHENTICATION_KEY); -@@ -446,6 +450,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -451,6 +455,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), @@ -7380,160 +8402,31 @@ index a18f61c64b..be0b77f4ca 100644 } diff --git a/js/gdm/webLogin.js b/js/gdm/webLogin.js new file mode 100644 -index 0000000000..2da29582f3 +index 0000000000..69717260fc --- /dev/null +++ b/js/gdm/webLogin.js -@@ -0,0 +1,435 @@ +@@ -0,0 +1,310 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- +// +// A widget showing a URL for web login +/* exported WebLoginPrompt */ + +import Clutter from 'gi://Clutter'; -+import Cogl from 'gi://Cogl'; +import GObject from 'gi://GObject'; -+import Gio from 'gi://Gio'; -+import GnomeQR from 'gi://GnomeQR'; +import Pango from 'gi://Pango'; +import St from 'gi://St'; +import {Spinner} from '../ui/animation.js'; +import * as Params from '../misc/params.js'; -+import {logErrorUnlessCancelled} from '../misc/errorUtils.js'; -+ -+Gio._promisify(GnomeQR, 'generate_qr_code_async'); ++import {QrCode} from '../ui/qrCode.js'; + +const QR_CODE_SIZE = 150; +const WEB_LOGIN_SPINNER_SIZE = 35; + -+export const QrCode = GObject.registerClass( -+class QrCode extends St.Bin { -+ _init(params) { -+ const themeContext = St.ThemeContext.get_for_stage(global.stage); -+ const {iconSize, url} = Params.parse(params, { -+ iconSize: QR_CODE_SIZE, -+ url: null, -+ }); -+ -+ super._init({ -+ width: QR_CODE_SIZE, -+ height: QR_CODE_SIZE, -+ x_align: Clutter.ActorAlign.CENTER, -+ }); -+ -+ this._bgColor = null; -+ this._fgColor = null; -+ -+ this._iconSize = iconSize; -+ this._url = url; -+ this.child = new St.Icon({ -+ icon_size: this._iconSize, -+ style_class: 'qr-code', -+ }); -+ -+ themeContext.connectObject('notify::scale-factor', -+ () => this.update(), this); -+ } -+ -+ vfunc_style_changed() { -+ super.vfunc_style_changed(); -+ -+ let changed = false; -+ const node = this.child.get_theme_node(); -+ const [found, iconSize] = node.lookup_length('icon-size', false); -+ -+ if (found) { -+ const themeContext = St.ThemeContext.get_for_stage(global.stage); -+ const newIconSize = iconSize / themeContext.scaleFactor; -+ if (this._iconSize !== newIconSize) { -+ this._iconSize = newIconSize; -+ changed = true; -+ } -+ } -+ -+ const bgColor = node.get_background_color(); -+ const fgColor = node.get_foreground_color(); -+ -+ if (!this._bgColor?.equal(bgColor)) { -+ this._bgColor = bgColor; -+ changed = true; -+ } -+ -+ if (!this._fgColor?.equal(fgColor)) { -+ this._fgColor = fgColor; -+ changed = true; -+ } -+ -+ if (!changed) -+ return; -+ -+ this.update(); -+ } -+ -+ setUrl(url) { -+ if (this._url === url) -+ return; -+ -+ this._url = url; -+ this.update(); -+ } -+ -+ async update() { -+ const {scaleFactor} = St.ThemeContext.get_for_stage(global.stage); -+ const iconSize = this._iconSize * scaleFactor; -+ const bgColor = this._getGnomeQRColor(this._bgColor); -+ const fgColor = this._getGnomeQRColor(this._fgColor); -+ -+ this._cancellable?.cancel(); -+ this._cancellable = new Gio.Cancellable(); -+ -+ try { -+ const [pixelData, qrSize] = await GnomeQR.generate_qr_code_async( -+ this._url, -+ iconSize, -+ bgColor, -+ fgColor, -+ GnomeQR.PixelFormat.RGBA_8888, -+ GnomeQR.EccLevel.LOW, -+ this._cancellable); -+ -+ const coglContext = global.stage.context.get_backend().get_cogl_context(); -+ const bpp = Cogl.pixel_format_get_bytes_per_pixel(Cogl.PixelFormat.RGBA_8888, 0); -+ const rowStride = qrSize * bpp; -+ -+ const content = St.ImageContent.new_with_preferred_size(qrSize, qrSize); -+ content.set_bytes( -+ coglContext, -+ pixelData, -+ Cogl.PixelFormat.RGBA_8888, -+ qrSize, -+ qrSize, -+ rowStride); -+ -+ this.set_size(qrSize, qrSize); -+ this.child.gicon = content; -+ } catch (e) { -+ logErrorUnlessCancelled(e, 'Failed to generate QR code'); -+ } -+ } -+ -+ _getGnomeQRColor(color) { -+ if (!color) -+ return null; -+ -+ return new GnomeQR.Color({ -+ red: color.red, -+ green: color.green, -+ blue: color.blue, -+ alpha: color.alpha, -+ }); -+ } -+}); -+ +export const WebLoginPrompt = GObject.registerClass( +class WebLoginPrompt extends St.BoxLayout { + _init(params) { -+ const {iconSize, message, url, code} = Params.parse(params, { -+ iconSize: QR_CODE_SIZE, ++ const {qrSize: qrCodeSize, message, url, code} = Params.parse(params, { ++ qrSize: QR_CODE_SIZE, + message: null, + url: null, + code: null, @@ -7554,7 +8447,11 @@ index 0000000000..2da29582f3 + }); + this.add_child(this._urlTitleLabel); + -+ this._qrCode = new QrCode({iconSize, url: null}); ++ this._qrCode = new QrCode({ ++ width: qrCodeSize, ++ height: qrCodeSize, ++ xAlign: Clutter.ActorAlign.CENTER, ++ }); + this.add_child(this._qrCode); + + this._urlLabel = new St.Label({ @@ -7593,7 +8490,7 @@ index 0000000000..2da29582f3 + this._urlTitleLabel.text = message; + + if (url !== null) { -+ this._qrCode.setUrl(url); ++ this._qrCode.set({url}); + + const formattedUrl = this._formatURLForDisplay(url); + this._urlLabel.text = formattedUrl; @@ -7820,7 +8717,7 @@ index 0000000000..2da29582f3 + } +}); diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml -index ca4856e565..0bdbbd8809 100644 +index 59a428df9a..85d8cc5c90 100644 --- a/js/js-resources.gresource.xml +++ b/js/js-resources.gresource.xml @@ -15,6 +15,7 @@ @@ -7831,29 +8728,17 @@ index ca4856e565..0bdbbd8809 100644 extensions/extension.js extensions/sharedInternals.js -diff --git a/js/misc/dependencies.js b/js/misc/dependencies.js -index c3c51aa2de..fbb83d1164 100644 ---- a/js/misc/dependencies.js -+++ b/js/misc/dependencies.js -@@ -16,6 +16,7 @@ import 'gi://GioUnix?version=2.0'; - import 'gi://GDesktopEnums?version=3.0'; - import 'gi://GdkPixbuf?version=2.0'; - import 'gi://GnomeBG?version=4.0'; -+import 'gi://GnomeQR?version=4.0'; - import 'gi://GnomeDesktop?version=4.0'; - import 'gi://Graphene?version=1.0'; - import 'gi://GUdev?version=1.0'; diff --git a/js/ui/unlockDialog.js b/js/ui/unlockDialog.js -index 90130269a9..0bf375ad18 100644 +index a16f68fee3..8a824f700a 100644 --- a/js/ui/unlockDialog.js +++ b/js/ui/unlockDialog.js -@@ -492,7 +492,8 @@ class UnlockDialogLayout extends Clutter.LayoutManager { - +@@ -494,7 +494,8 @@ class UnlockDialogLayout extends Clutter.LayoutManager { // Authentication Box + const dialog = container.get_parent(); let stackY; -- if (this._activePage === this._clock) { -+ if (this._activePage === this._clock || -+ this._authPrompt?.has_style_class_name('web-login-active')) { +- 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); @@ -7875,11 +8760,11 @@ index 0331a1d328..e0ffd32278 100644 + } }); diff --git a/po/POTFILES.in b/po/POTFILES.in -index 6c4ed8012e..12001308f3 100644 +index d1e4d64872..9a7f8db8ea 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in -@@ -16,6 +16,7 @@ js/gdm/authServicesLegacy.js - js/gdm/authServicesSwitchable.js +@@ -15,6 +15,7 @@ js/gdm/authServicesLegacy.js + js/gdm/authServicesSSSDSwitchable.js js/gdm/loginDialog.js js/gdm/util.js +js/gdm/webLogin.js @@ -7890,36 +8775,36 @@ index 6c4ed8012e..12001308f3 100644 2.53.0 -From 949d002a5b8c51123efe21bfbab964cfe911ad36 Mon Sep 17 00:00:00 2001 +From 675ebdd383025c8b3a7ee76dbe81fb0f5e6353e4 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Tue, 14 Jan 2025 07:31:59 -0500 -Subject: [PATCH 28/30] gdm: Add support for Smartcard in - authServicesSwitchable +Subject: [PATCH 41/42] 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 +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. -When a smartcard is inserted, authServicesSwitchable will be restarted to +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/authServicesSwitchable.js | 64 ++++++++++++++++++++++++++++++++ - js/misc/smartcardManager.js | 6 +++ - 2 files changed, 70 insertions(+) + js/gdm/authServicesSSSDSwitchable.js | 70 ++++++++++++++++++++++++++++ + js/misc/smartcardManager.js | 6 +++ + 2 files changed, 76 insertions(+) -diff --git a/js/gdm/authServicesSwitchable.js b/js/gdm/authServicesSwitchable.js -index d13020e642..b99128dc15 100644 ---- a/js/gdm/authServicesSwitchable.js -+++ b/js/gdm/authServicesSwitchable.js -@@ -15,11 +15,13 @@ export const AuthServicesSwitchable = GObject.registerClass({ - }, class AuthServicesSwitchable extends AuthServices { +diff --git a/js/gdm/authServicesSSSDSwitchable.js b/js/gdm/authServicesSSSDSwitchable.js +index 98171b32c2..b0c3583786 100644 +--- a/js/gdm/authServicesSSSDSwitchable.js ++++ b/js/gdm/authServicesSSSDSwitchable.js +@@ -16,11 +16,13 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + static SupportedRoles = [ Const.PASSWORD_ROLE_NAME, + Const.SMARTCARD_ROLE_NAME, @@ -7932,7 +8817,7 @@ index d13020e642..b99128dc15 100644 [Const.WEB_LOGIN_ROLE_NAME]: Const.SWITCHABLE_AUTH_SERVICE_NAME, }; -@@ -29,6 +31,18 @@ export const AuthServicesSwitchable = GObject.registerClass({ +@@ -34,6 +36,18 @@ export class AuthServicesSSSDSwitchable extends AuthServices { this._mechanismsStatus = MechanismsStatus.WAITING; } @@ -7951,7 +8836,7 @@ index d13020e642..b99128dc15 100644 async _handleAnswerQuery(serviceName, answer) { if (serviceName !== this._selectedMechanism?.serviceName) return; -@@ -45,6 +59,7 @@ export const AuthServicesSwitchable = GObject.registerClass({ +@@ -50,6 +64,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { let response; switch (this._selectedMechanism.role) { case Const.PASSWORD_ROLE_NAME: @@ -7959,7 +8844,7 @@ index d13020e642..b99128dc15 100644 response = this._formatResponse(answer); this._sendResponse(response); break; -@@ -56,6 +71,9 @@ export const AuthServicesSwitchable = GObject.registerClass({ +@@ -61,6 +76,9 @@ export class AuthServicesSSSDSwitchable extends AuthServices { case Const.PASSWORD_ROLE_NAME: this._startPasswordLogin(); break; @@ -7969,7 +8854,7 @@ index d13020e642..b99128dc15 100644 case Const.WEB_LOGIN_ROLE_NAME: this._startWebLogin(); break; -@@ -93,6 +111,7 @@ export const AuthServicesSwitchable = GObject.registerClass({ +@@ -98,6 +116,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { this._priorityList = null; this._enabledMechanisms = null; this._selectedMechanism = null; @@ -7977,7 +8862,7 @@ index d13020e642..b99128dc15 100644 this._resettingPassword = false; -@@ -171,6 +190,14 @@ export const AuthServicesSwitchable = GObject.registerClass({ +@@ -176,6 +195,14 @@ export class AuthServicesSSSDSwitchable extends AuthServices { }); } @@ -7992,7 +8877,7 @@ index d13020e642..b99128dc15 100644 _handleOnInfo(serviceName, info) { if (!this._eventExpected()) return; -@@ -239,6 +266,11 @@ export const AuthServicesSwitchable = GObject.registerClass({ +@@ -244,6 +271,11 @@ export class AuthServicesSSSDSwitchable extends AuthServices { response = {password: answer}; break; } @@ -8004,7 +8889,7 @@ index d13020e642..b99128dc15 100644 case Const.WEB_LOGIN_ROLE_NAME: { response = {}; break; -@@ -282,6 +314,38 @@ export const AuthServicesSwitchable = GObject.registerClass({ +@@ -287,6 +319,44 @@ export class AuthServicesSSSDSwitchable extends AuthServices { this.emit('ask-question', serviceName, prompt, true); } @@ -8037,7 +8922,13 @@ index d13020e642..b99128dc15 100644 + const commonName = fields.find(f => f.startsWith('CN='))?.substring(3); + const organization = fields.find(f => f.startsWith('O='))?.substring(2); + -+ return {description, commonName, organization}; ++ return { ++ title: commonName, ++ subtitle: description, ++ iconName: organization ? 'vcard-symbolic' : null, ++ iconTitle: organization ? _('Organization') : null, ++ iconSubtitle: organization, ++ }; + } + _startWebLogin() { @@ -8064,10 +8955,11 @@ index 51471e51d4..3eabd2f3be 100644 2.53.0 -From 321cb17c87a686820e44abfaafb358e973055d1d Mon Sep 17 00:00:00 2001 +From 36c26f28f6e5b1883267a12b3e76c3fd0c888cb3 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Mon, 15 Sep 2025 16:36:42 +0200 -Subject: [PATCH 29/30] gdm: Add support for Passkey in authServicesSwitchable +Subject: [PATCH 42/42] gdm: Add support for Passkey in + authServicesSSSDSwitchable This allows selecting passkey authentication mechanism. @@ -8078,40 +8970,40 @@ 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/authServicesSwitchable.js | 46 +++++++++++++++++++++++++++++++- - js/gdm/const.js | 1 + - js/gdm/util.js | 4 +++ - js/ui/unlockDialog.js | 2 ++ + js/gdm/authServices.js | 12 ++++++++ + js/gdm/authServicesSSSDSwitchable.js | 46 +++++++++++++++++++++++++++- + js/gdm/const.js | 1 + + js/gdm/util.js | 4 +++ + js/ui/unlockDialog.js | 2 ++ 5 files changed, 64 insertions(+), 1 deletion(-) diff --git a/js/gdm/authServices.js b/js/gdm/authServices.js -index 066276b786..2c5f96ff57 100644 +index 1801fbdc3a..e1039485a3 100644 --- a/js/gdm/authServices.js +++ b/js/gdm/authServices.js @@ -2,6 +2,7 @@ import * as FingerprintManager from '../misc/fingerprintManager.js'; import * as Params from '../misc/params.js'; -+import * as PasskeyManager from '../misc/passkeyManager.js'; ++import * as PasskeyDeviceManager from '../misc/passkeyDeviceManager.js'; import * as SmartcardManager from '../misc/smartcardManager.js'; import {logErrorUnlessCancelled} from '../misc/errorUtils.js'; import * as Util from './util.js'; -@@ -78,6 +79,7 @@ export const AuthServices = GObject.registerClass({ +@@ -85,6 +86,7 @@ export class AuthServices extends GObject.Object { this._cancellable = null; this._connectSmartcardManager(); -+ this._connectPasskeyManager(); ++ this._connectPasskeyDeviceManager(); this._connectFingerprintManager(); } -@@ -218,6 +220,14 @@ export const AuthServices = GObject.registerClass({ +@@ -225,6 +227,14 @@ export class AuthServices extends GObject.Object { this); } -+ _connectPasskeyManager() { -+ this._passkeyManager = PasskeyManager.getPasskeyManager(); -+ this._passkeyManager.connectObject( ++ _connectPasskeyDeviceManager() { ++ this._passkeyDeviceManager = PasskeyDeviceManager.getPasskeyDeviceManager(); ++ this._passkeyDeviceManager.connectObject( + 'passkey-inserted', () => this._handlePasskeyChanged(), + 'passkey-removed', () => this._handlePasskeyChanged(), + this); @@ -8120,7 +9012,7 @@ index 066276b786..2c5f96ff57 100644 _connectFingerprintManager() { // Fingerprint can only work on lockscreen if (!this._reauthOnly) -@@ -459,6 +469,8 @@ export const AuthServices = GObject.registerClass({ +@@ -469,6 +479,8 @@ export class AuthServices extends GObject.Object { _handleSmartcardChanged() {} @@ -8129,11 +9021,11 @@ index 066276b786..2c5f96ff57 100644 _handleFingerprintChanged() {} _handleOnInfo() {} -diff --git a/js/gdm/authServicesSwitchable.js b/js/gdm/authServicesSwitchable.js -index b99128dc15..eb90ad7ece 100644 ---- a/js/gdm/authServicesSwitchable.js -+++ b/js/gdm/authServicesSwitchable.js -@@ -16,12 +16,14 @@ export const AuthServicesSwitchable = GObject.registerClass({ +diff --git a/js/gdm/authServicesSSSDSwitchable.js b/js/gdm/authServicesSSSDSwitchable.js +index b0c3583786..9c8464ac9d 100644 +--- a/js/gdm/authServicesSSSDSwitchable.js ++++ b/js/gdm/authServicesSSSDSwitchable.js +@@ -17,12 +17,14 @@ export class AuthServicesSSSDSwitchable extends AuthServices { static SupportedRoles = [ Const.PASSWORD_ROLE_NAME, Const.SMARTCARD_ROLE_NAME, @@ -8148,7 +9040,7 @@ index b99128dc15..eb90ad7ece 100644 [Const.WEB_LOGIN_ROLE_NAME]: Const.SWITCHABLE_AUTH_SERVICE_NAME, }; -@@ -63,6 +65,13 @@ export const AuthServicesSwitchable = GObject.registerClass({ +@@ -68,6 +70,13 @@ export class AuthServicesSSSDSwitchable extends AuthServices { response = this._formatResponse(answer); this._sendResponse(response); break; @@ -8162,7 +9054,7 @@ index b99128dc15..eb90ad7ece 100644 } } -@@ -74,6 +83,9 @@ export const AuthServicesSwitchable = GObject.registerClass({ +@@ -79,6 +88,9 @@ export class AuthServicesSSSDSwitchable extends AuthServices { case Const.SMARTCARD_ROLE_NAME: this._startSmartcardLogin(); break; @@ -8172,7 +9064,7 @@ index b99128dc15..eb90ad7ece 100644 case Const.WEB_LOGIN_ROLE_NAME: this._startWebLogin(); break; -@@ -198,6 +210,14 @@ export const AuthServicesSwitchable = GObject.registerClass({ +@@ -203,6 +215,14 @@ export class AuthServicesSSSDSwitchable extends AuthServices { this.emit('reset', {softReset: true, reuseEntryText: true}); } @@ -8187,7 +9079,7 @@ index b99128dc15..eb90ad7ece 100644 _handleOnInfo(serviceName, info) { if (!this._eventExpected()) return; -@@ -258,7 +278,7 @@ export const AuthServicesSwitchable = GObject.registerClass({ +@@ -263,7 +283,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { } _formatResponse(answer) { @@ -8196,7 +9088,7 @@ index b99128dc15..eb90ad7ece 100644 let response; switch (role) { -@@ -271,6 +291,10 @@ export const AuthServicesSwitchable = GObject.registerClass({ +@@ -276,6 +296,10 @@ export class AuthServicesSSSDSwitchable extends AuthServices { response = {pin: answer, tokenName, moduleName, keyId, label}; break; } @@ -8207,8 +9099,8 @@ index b99128dc15..eb90ad7ece 100644 case Const.WEB_LOGIN_ROLE_NAME: { response = {}; break; -@@ -346,6 +370,26 @@ export const AuthServicesSwitchable = GObject.registerClass({ - return {description, commonName, organization}; +@@ -357,6 +381,26 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + }; } + _startPasskeyLogin() { @@ -8247,7 +9139,7 @@ index e8ca48625a..7c48599e64 100644 export const PASSWORD_SERVICE_NAME = 'gdm-password'; diff --git a/js/gdm/util.js b/js/gdm/util.js -index be0b77f4ca..71e28c660e 100644 +index fe2216f8c7..5baa676a6a 100644 --- a/js/gdm/util.js +++ b/js/gdm/util.js @@ -16,6 +16,7 @@ export const LOGIN_SCREEN_SCHEMA = 'org.gnome.login-screen'; @@ -8258,7 +9150,7 @@ index be0b77f4ca..71e28c660e 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'; -@@ -94,6 +95,7 @@ export function isSelectable(mechanism) { +@@ -95,6 +96,7 @@ export function isSelectable(mechanism) { switch (mechanism.role) { case Const.PASSWORD_ROLE_NAME: case Const.SMARTCARD_ROLE_NAME: @@ -8266,7 +9158,7 @@ index be0b77f4ca..71e28c660e 100644 case Const.WEB_LOGIN_ROLE_NAME: return true; case Const.FINGERPRINT_ROLE_NAME: -@@ -384,6 +386,8 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -389,6 +391,8 @@ export class ShellUserVerifier extends Signals.EventEmitter { enabledRoles.push(Const.PASSWORD_ROLE_NAME); if (this._settings.get_boolean(SMARTCARD_AUTHENTICATION_KEY)) enabledRoles.push(Const.SMARTCARD_ROLE_NAME); @@ -8276,10 +9168,10 @@ index be0b77f4ca..71e28c660e 100644 enabledRoles.push(Const.FINGERPRINT_ROLE_NAME); if (this._settings.get_boolean(WEB_AUTHENTICATION_KEY)) diff --git a/js/ui/unlockDialog.js b/js/ui/unlockDialog.js -index 0bf375ad18..e506acfad8 100644 +index 8a824f700a..16ac696029 100644 --- a/js/ui/unlockDialog.js +++ b/js/ui/unlockDialog.js -@@ -426,6 +426,8 @@ class UnlockDialogClock extends St.BoxLayout { +@@ -428,6 +428,8 @@ class UnlockDialogClock extends St.BoxLayout { if (authMechanism?.role === GdmConst.SMARTCARD_ROLE_NAME) text = _('Insert smartcard'); @@ -8291,71 +9183,3 @@ index 0bf375ad18..e506acfad8 100644 -- 2.53.0 - -From 681639f743087f8b4fca5b302f3a0e2e3bd7b50d Mon Sep 17 00:00:00 2001 -From: Joan Torres Lopez -Date: Wed, 24 Dec 2025 11:28:39 +0100 -Subject: [PATCH 30/30] gdm: Add extra fix remove logErrorUnlessCancelled - ---- - js/gdm/authServices.js | 7 ++++--- - js/gdm/webLogin.js | 4 ++-- - 2 files changed, 6 insertions(+), 5 deletions(-) - -diff --git a/js/gdm/authServices.js b/js/gdm/authServices.js -index 2c5f96ff57..7102e76149 100644 ---- a/js/gdm/authServices.js -+++ b/js/gdm/authServices.js -@@ -4,7 +4,6 @@ import * as FingerprintManager from '../misc/fingerprintManager.js'; - import * as Params from '../misc/params.js'; - import * as PasskeyManager from '../misc/passkeyManager.js'; - import * as SmartcardManager from '../misc/smartcardManager.js'; --import {logErrorUnlessCancelled} from '../misc/errorUtils.js'; - import * as Util from './util.js'; - import Gdm from 'gi://Gdm'; - import GLib from 'gi://GLib'; -@@ -112,7 +111,8 @@ export const AuthServices = GObject.registerClass({ - await this._waitPendingMessages(); - await this._handleAnswerQuery(serviceName, answer); - } catch (e) { -- logErrorUnlessCancelled(e); -+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) -+ logError(e); - } - } - -@@ -381,7 +381,8 @@ export const AuthServices = GObject.registerClass({ - await this._waitPendingMessages(); - this.emit('reset', {softReset: !doneTrying}); - } catch (e) { -- logErrorUnlessCancelled(e); -+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) -+ logError(e); - } - } - -diff --git a/js/gdm/webLogin.js b/js/gdm/webLogin.js -index 2da29582f3..5a864587dc 100644 ---- a/js/gdm/webLogin.js -+++ b/js/gdm/webLogin.js -@@ -12,7 +12,6 @@ import Pango from 'gi://Pango'; - import St from 'gi://St'; - import {Spinner} from '../ui/animation.js'; - import * as Params from '../misc/params.js'; --import {logErrorUnlessCancelled} from '../misc/errorUtils.js'; - - Gio._promisify(GnomeQR, 'generate_qr_code_async'); - -@@ -126,7 +125,8 @@ class QrCode extends St.Bin { - this.set_size(qrSize, qrSize); - this.child.gicon = content; - } catch (e) { -- logErrorUnlessCancelled(e, 'Failed to generate QR code'); -+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) -+ logError(e); - } - } - --- -2.53.0 - diff --git a/0001-data-Update-generated-stylesheets.patch b/0001-data-Update-generated-stylesheets.patch index abb49dd..15cc6e2 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 | 460 +++++++++++++++++----- - data/theme/gnome-shell-high-contrast.css | 476 ++++++++++++++++++----- - data/theme/gnome-shell-light.css | 460 +++++++++++++++++----- - 3 files changed, 1072 insertions(+), 324 deletions(-) + 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(-) diff --git a/data/theme/gnome-shell-dark.css b/data/theme/gnome-shell-dark.css -index 75dc5c6..aa5765d 100644 +index 82e68c5..a96fde1 100644 --- a/data/theme/gnome-shell-dark.css +++ b/data/theme/gnome-shell-dark.css @@ -42,7 +42,9 @@ stage { @@ -215,8 +215,8 @@ index 75dc5c6..aa5765d 100644 font-size: 1.182em; } -.login-dialog-not-listed-label, #LookingGlassExtensions .lg-extension .lg-extension-name, #LookingGlassWindows .lg-window .lg-window-name, #LookingGlassPropertyInspector .lg-obj-inspector-title, .background-app-item .title, .quick-toggle .quick-toggle-title, .osd-window, .dialog-list .dialog-list-title, .message-list-controls, .weather-button .weather-box .weather-header-box .weather-header, .world-clocks-button .world-clocks-header, .events-button .events-box .events-list .event-box .event-summary, .events-button .events-box .events-title, .calendar .calendar-month-header .calendar-month-label { -+.login-dialog-auth-list-item-popup-box .login-dialog-auth-list-item-popup-labels, .login-dialog-auth-list-item-first-line, -+.login-dialog-auth-list-item-second-line, .login-dialog-not-listed-label, #LookingGlassExtensions .lg-extension .lg-extension-name, #LookingGlassWindows .lg-window .lg-window-name, #LookingGlassPropertyInspector .lg-obj-inspector-title, .background-app-item .title, .quick-toggle .quick-toggle-title, .osd-window, .dialog-list .dialog-list-title, .message-list-controls, .weather-button .weather-box .weather-header-box .weather-header, .world-clocks-button .world-clocks-header, .events-button .events-box .events-list .event-box .event-summary, .events-button .events-box .events-title, .calendar .calendar-month-header .calendar-month-label { ++.login-dialog-item-icon-popup-box .login-dialog-item-icon-popup-labels, .login-dialog-auth-list-item-title, ++.login-dialog-auth-list-item-subtitle, .login-dialog-not-listed-label, #LookingGlassExtensions .lg-extension .lg-extension-name, #LookingGlassWindows .lg-window .lg-window-name, #LookingGlassPropertyInspector .lg-obj-inspector-title, .background-app-item .title, .quick-toggle .quick-toggle-title, .osd-window, .dialog-list .dialog-list-title, .message-list-controls, .weather-button .weather-box .weather-header-box .weather-header, .world-clocks-button .world-clocks-header, .events-button .events-box .events-list .event-box .event-summary, .events-button .events-box .events-title, .calendar .calendar-month-header .calendar-month-label { font-weight: 700; font-size: 1em; } @@ -553,7 +553,21 @@ index 75dc5c6..aa5765d 100644 .app-folder-dialog .message .message-header .message-close-button:active:focus, .message .message-header .app-folder-dialog .message-close-button:active:focus, .app-folder-dialog .screenshot-ui-show-pointer-button:active:focus { background-color: st-mix(st-mix(-st-accent-color, #ffffff, 60%), st-lighten(st-mix(#fafafb, #38383b, 9%), 9%), 5%); } -@@ -3044,19 +3085,62 @@ StScrollBar { +@@ -3032,6 +3073,13 @@ StScrollBar { + background-color: st-lighten(-st-accent-color, 5%); + color: st-lighten(-st-accent-fg-color, 5%); } + ++.qr-code { ++ border-radius: 4px; ++ border-width: 1em; ++ background-color: #fafafb; ++ border-color: #fafafb; ++ color: #2e2e33; } ++ + .login-dialog, + .unlock-dialog { + color: #fafafb; } +@@ -3044,19 +3092,62 @@ StScrollBar { .unlock-dialog .login-dialog-prompt-layout { width: 25em; spacing: 9px; } @@ -619,7 +633,7 @@ index 75dc5c6..aa5765d 100644 .conflicting-session-dialog-content { spacing: 20px; } -@@ -3118,49 +3202,103 @@ StScrollBar { +@@ -3118,49 +3209,104 @@ StScrollBar { background-color: st-mix(st-mix(-st-accent-color, #ffffff, 60%), st-lighten(#222226, 9%), 5%); } .login-dialog-auth-list-view { @@ -706,50 +720,51 @@ index 75dc5c6..aa5765d 100644 + color: #fafafb !important; + padding: 0; + margin: 0; } -+ -+.login-dialog-auth-list-title-label { -+ padding: 6px; -+ text-align: center; } -+ -+.login-dialog-auth-list-item-first-line, -+.login-dialog-auth-list-item-second-line { -+ text-align: center; -+ padding: 1.8px 0; } -+ -+.login-dialog-auth-list-item-first-line { -+ color: #fafafb; } -.login-dialog-auth-list-label:ltr { - padding-left: 15px; - text-align: left; } -+.login-dialog-auth-list-item-second-line { ++.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-item-title, ++.login-dialog-auth-list-item-subtitle { ++ text-align: center; ++ padding: 1.8px 0; } ++ ++.login-dialog-auth-list-item-title { ++ color: #fafafb; } ++ ++.login-dialog-auth-list-item-subtitle { + color: #c1c1ce; + font-weight: 500; } + -+.login-dialog-auth-list-item-icon { ++.login-dialog-item-icon { + width: 1.3em; + height: 1.3em; + color: #fafafb; + padding: 4.2px 6px; + border-radius: 8px; } -+ .login-dialog-auth-list-item-icon:hover { ++ .login-dialog-item-icon:hover { + background-color: #2e2e33; } - --.login-dialog-auth-list-label:rtl { -- padding-right: 15px; -- text-align: right; } -+.login-dialog-auth-list-item-popup-box .login-dialog-auth-list-item-popup-labels { -+ spacing: 3px; -+ text-align: center; } -+ .login-dialog-auth-list-item-popup-box .login-dialog-auth-list-item-popup-labels > :first-child { -+ color: #b3b3b3; -+ font-weight: 500; } -+ .login-dialog-auth-list-item-popup-box .login-dialog-auth-list-item-popup-labels > :last-child { -+ color: #ffffff; } ++ .login-dialog-item-icon-popup.popup-menu { ++ min-width: 0; } ++ .login-dialog-item-icon-popup-box .login-dialog-item-icon-popup-labels { ++ spacing: 3px; ++ text-align: center; } ++ .login-dialog-item-icon-popup-box .login-dialog-item-icon-popup-labels > :first-child { ++ color: #b3b3b3; ++ font-weight: 500; } ++ .login-dialog-item-icon-popup-box .login-dialog-item-icon-popup-labels > :last-child { ++ color: #ffffff; } .login-dialog-user-list-view { width: 25em; -@@ -3189,6 +3327,9 @@ StScrollBar { +@@ -3189,6 +3335,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%); } @@ -759,7 +774,7 @@ index 75dc5c6..aa5765d 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 +3341,102 @@ StScrollBar { +@@ -3200,6 +3349,102 @@ 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); } @@ -862,7 +877,7 @@ index 75dc5c6..aa5765d 100644 .unlock-dialog { background-color: transparent; } -@@ -3308,3 +3545,10 @@ StScrollBar { +@@ -3308,3 +3553,10 @@ StScrollBar { .login-dialog .user-widget.vertical .user-icon StIcon, .unlock-dialog .user-widget.vertical .user-icon StIcon { padding: 30px; } @@ -874,7 +889,7 @@ index 75dc5c6..aa5765d 100644 + border-color: #fafafb; + color: #222226; } diff --git a/data/theme/gnome-shell-high-contrast.css b/data/theme/gnome-shell-high-contrast.css -index 69749c1..af1dffc 100644 +index b69823f..0ed28a3 100644 --- a/data/theme/gnome-shell-high-contrast.css +++ b/data/theme/gnome-shell-high-contrast.css @@ -42,7 +42,9 @@ stage { @@ -1118,8 +1133,8 @@ index 69749c1..af1dffc 100644 font-size: 1.182em; } -.login-dialog-not-listed-label, #LookingGlassExtensions .lg-extension .lg-extension-name, #LookingGlassWindows .lg-window .lg-window-name, #LookingGlassPropertyInspector .lg-obj-inspector-title, .background-app-item .title, .quick-toggle .quick-toggle-title, .osd-window, .dialog-list .dialog-list-title, .message-list-controls, .weather-button .weather-box .weather-header-box .weather-header, .world-clocks-button .world-clocks-header, .events-button .events-box .events-list .event-box .event-summary, .events-button .events-box .events-title, .calendar .calendar-month-header .calendar-month-label { -+.login-dialog-auth-list-item-popup-box .login-dialog-auth-list-item-popup-labels, .login-dialog-auth-list-item-first-line, -+.login-dialog-auth-list-item-second-line, .login-dialog-not-listed-label, #LookingGlassExtensions .lg-extension .lg-extension-name, #LookingGlassWindows .lg-window .lg-window-name, #LookingGlassPropertyInspector .lg-obj-inspector-title, .background-app-item .title, .quick-toggle .quick-toggle-title, .osd-window, .dialog-list .dialog-list-title, .message-list-controls, .weather-button .weather-box .weather-header-box .weather-header, .world-clocks-button .world-clocks-header, .events-button .events-box .events-list .event-box .event-summary, .events-button .events-box .events-title, .calendar .calendar-month-header .calendar-month-label { ++.login-dialog-item-icon-popup-box .login-dialog-item-icon-popup-labels, .login-dialog-auth-list-item-title, ++.login-dialog-auth-list-item-subtitle, .login-dialog-not-listed-label, #LookingGlassExtensions .lg-extension .lg-extension-name, #LookingGlassWindows .lg-window .lg-window-name, #LookingGlassPropertyInspector .lg-obj-inspector-title, .background-app-item .title, .quick-toggle .quick-toggle-title, .osd-window, .dialog-list .dialog-list-title, .message-list-controls, .weather-button .weather-box .weather-header-box .weather-header, .world-clocks-button .world-clocks-header, .events-button .events-box .events-list .event-box .event-summary, .events-button .events-box .events-title, .calendar .calendar-month-header .calendar-month-label { font-weight: 700; font-size: 1em; } @@ -1469,7 +1484,21 @@ index 69749c1..af1dffc 100644 .app-folder-dialog .message .message-header .message-close-button:active:focus, .message .message-header .app-folder-dialog .message-close-button:active:focus, .app-folder-dialog .screenshot-ui-show-pointer-button:active:focus { background-color: st-mix(st-mix(-st-accent-color, #ffffff, 60%), st-mix(st-lighten(st-mix(#ffffff, #252525, 9%), 9%), #ffffff, 87%), 5%); } -@@ -3374,19 +3415,62 @@ StScrollBar { +@@ -3362,6 +3403,13 @@ StScrollBar { + background-color: st-lighten(-st-accent-color, 5%); + color: st-lighten(-st-accent-fg-color, 5%); } + ++.qr-code { ++ border-radius: 4px; ++ border-width: 1em; ++ background-color: #ffffff; ++ border-color: #ffffff; ++ color: #0d0d0d; } ++ + .login-dialog, + .unlock-dialog { + color: #ffffff; } +@@ -3374,19 +3422,62 @@ StScrollBar { .unlock-dialog .login-dialog-prompt-layout { width: 25em; spacing: 9px; } @@ -1535,7 +1564,7 @@ index 69749c1..af1dffc 100644 .conflicting-session-dialog-content { spacing: 20px; } -@@ -3455,53 +3539,111 @@ StScrollBar { +@@ -3455,53 +3546,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 { @@ -1630,50 +1659,51 @@ index 69749c1..af1dffc 100644 + 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; } + -+.login-dialog-auth-list-item-first-line, -+.login-dialog-auth-list-item-second-line { ++.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; } + -.login-dialog-auth-list-label:rtl { - padding-right: 15px; - text-align: right; } -+.login-dialog-auth-list-item-first-line { -+ color: #ffffff; } -+ -+.login-dialog-auth-list-item-second-line { ++.login-dialog-auth-list-item-subtitle { + color: #cccccc; + font-weight: 500; } + -+.login-dialog-auth-list-item-icon { ++.login-dialog-item-icon { + width: 1.3em; + height: 1.3em; + color: #ffffff; + padding: 4.2px 6px; + border-radius: 8px; } -+ .login-dialog-auth-list-item-icon:hover { ++ .login-dialog-item-icon:hover { + background-color: #0d0d0d; } -+ -+.login-dialog-auth-list-item-popup-box .login-dialog-auth-list-item-popup-labels { -+ spacing: 3px; -+ text-align: center; } -+ .login-dialog-auth-list-item-popup-box .login-dialog-auth-list-item-popup-labels > :first-child { -+ color: #b3b3b3; -+ font-weight: 500; } -+ .login-dialog-auth-list-item-popup-box .login-dialog-auth-list-item-popup-labels > :last-child { -+ color: #ffffff; } ++ .login-dialog-item-icon-popup.popup-menu { ++ min-width: 0; } ++ .login-dialog-item-icon-popup-box .login-dialog-item-icon-popup-labels { ++ spacing: 3px; ++ text-align: center; } ++ .login-dialog-item-icon-popup-box .login-dialog-item-icon-popup-labels > :first-child { ++ color: #b3b3b3; ++ font-weight: 500; } ++ .login-dialog-item-icon-popup-box .login-dialog-item-icon-popup-labels > :last-child { ++ color: #ffffff; } .login-dialog-user-list-view { width: 25em; -@@ -3534,6 +3676,13 @@ StScrollBar { +@@ -3534,6 +3684,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%); } @@ -1687,7 +1717,7 @@ index 69749c1..af1dffc 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 +3694,110 @@ StScrollBar { +@@ -3545,6 +3702,110 @@ 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); } @@ -1798,7 +1828,7 @@ index 69749c1..af1dffc 100644 .unlock-dialog { background-color: transparent; } -@@ -3655,3 +3908,10 @@ StScrollBar { +@@ -3655,3 +3916,10 @@ StScrollBar { .login-dialog .user-widget.vertical .user-icon StIcon, .unlock-dialog .user-widget.vertical .user-icon StIcon { padding: 30px; } @@ -1810,7 +1840,7 @@ index 69749c1..af1dffc 100644 + border-color: #ffffff; + color: #000000; } diff --git a/data/theme/gnome-shell-light.css b/data/theme/gnome-shell-light.css -index 174d723..fa83485 100644 +index 2b67846..792690e 100644 --- a/data/theme/gnome-shell-light.css +++ b/data/theme/gnome-shell-light.css @@ -42,7 +42,9 @@ stage { @@ -2015,8 +2045,8 @@ index 174d723..fa83485 100644 font-size: 1.182em; } -.login-dialog-not-listed-label, #LookingGlassExtensions .lg-extension .lg-extension-name, #LookingGlassWindows .lg-window .lg-window-name, #LookingGlassPropertyInspector .lg-obj-inspector-title, .background-app-item .title, .quick-toggle .quick-toggle-title, .osd-window, .dialog-list .dialog-list-title, .message-list-controls, .weather-button .weather-box .weather-header-box .weather-header, .world-clocks-button .world-clocks-header, .events-button .events-box .events-list .event-box .event-summary, .events-button .events-box .events-title, .calendar .calendar-month-header .calendar-month-label { -+.login-dialog-auth-list-item-popup-box .login-dialog-auth-list-item-popup-labels, .login-dialog-auth-list-item-first-line, -+.login-dialog-auth-list-item-second-line, .login-dialog-not-listed-label, #LookingGlassExtensions .lg-extension .lg-extension-name, #LookingGlassWindows .lg-window .lg-window-name, #LookingGlassPropertyInspector .lg-obj-inspector-title, .background-app-item .title, .quick-toggle .quick-toggle-title, .osd-window, .dialog-list .dialog-list-title, .message-list-controls, .weather-button .weather-box .weather-header-box .weather-header, .world-clocks-button .world-clocks-header, .events-button .events-box .events-list .event-box .event-summary, .events-button .events-box .events-title, .calendar .calendar-month-header .calendar-month-label { ++.login-dialog-item-icon-popup-box .login-dialog-item-icon-popup-labels, .login-dialog-auth-list-item-title, ++.login-dialog-auth-list-item-subtitle, .login-dialog-not-listed-label, #LookingGlassExtensions .lg-extension .lg-extension-name, #LookingGlassWindows .lg-window .lg-window-name, #LookingGlassPropertyInspector .lg-obj-inspector-title, .background-app-item .title, .quick-toggle .quick-toggle-title, .osd-window, .dialog-list .dialog-list-title, .message-list-controls, .weather-button .weather-box .weather-header-box .weather-header, .world-clocks-button .world-clocks-header, .events-button .events-box .events-list .event-box .event-summary, .events-button .events-box .events-title, .calendar .calendar-month-header .calendar-month-label { font-weight: 700; font-size: 1em; } @@ -2353,7 +2383,20 @@ index 174d723..fa83485 100644 .app-folder-dialog .message .message-header .message-close-button:active:focus, .message .message-header .app-folder-dialog .message-close-button:active:focus, .app-folder-dialog .screenshot-ui-show-pointer-button:active:focus { background-color: st-mix(-st-accent-color, st-lighten(st-mix(#fafafb, #38383b, 12%), 9%), 5%); } -@@ -3045,19 +3086,62 @@ StScrollBar { +@@ -3033,6 +3074,12 @@ StScrollBar { + background-color: st-lighten(-st-accent-color, 5%); + color: st-lighten(-st-accent-fg-color, 5%); } + ++.qr-code { ++ border-radius: 4px; ++ border-width: 1em; ++ background-color: #e9e9ea; ++ border-color: #e9e9ea; } ++ + .login-dialog, + .unlock-dialog { + color: #fafafb; } +@@ -3045,19 +3092,62 @@ StScrollBar { .unlock-dialog .login-dialog-prompt-layout { width: 25em; spacing: 9px; } @@ -2419,7 +2462,7 @@ index 174d723..fa83485 100644 .conflicting-session-dialog-content { spacing: 20px; } -@@ -3119,49 +3203,103 @@ StScrollBar { +@@ -3119,49 +3209,104 @@ StScrollBar { background-color: st-mix(-st-accent-color, st-lighten(#222226, 9%), 5%); } .login-dialog-auth-list-view { @@ -2506,50 +2549,51 @@ index 174d723..fa83485 100644 + color: #fafafb !important; + padding: 0; + margin: 0; } -+ -+.login-dialog-auth-list-title-label { -+ padding: 6px; -+ text-align: center; } -+ -+.login-dialog-auth-list-item-first-line, -+.login-dialog-auth-list-item-second-line { -+ text-align: center; -+ padding: 1.8px 0; } -+ -+.login-dialog-auth-list-item-first-line { -+ color: #fafafb; } -.login-dialog-auth-list-label:ltr { - padding-left: 15px; - text-align: left; } -+.login-dialog-auth-list-item-second-line { -+ color: #c1c1ce; -+ font-weight: 500; } ++.login-dialog-auth-list-title-label { ++ padding: 6px; ++ text-align: center; } ++ ++.login-dialog-auth-list-item-title, ++.login-dialog-auth-list-item-subtitle { ++ text-align: center; ++ padding: 1.8px 0; } ++ ++.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-icon { ++.login-dialog-auth-list-item-subtitle { ++ color: #c1c1ce; ++ font-weight: 500; } ++ ++.login-dialog-item-icon { + width: 1.3em; + height: 1.3em; + color: #fafafb; + padding: 4.2px 6px; + border-radius: 8px; } -+ .login-dialog-auth-list-item-icon:hover { ++ .login-dialog-item-icon:hover { + background-color: #2e2e33; } -+ -+.login-dialog-auth-list-item-popup-box .login-dialog-auth-list-item-popup-labels { -+ spacing: 3px; -+ text-align: center; } -+ .login-dialog-auth-list-item-popup-box .login-dialog-auth-list-item-popup-labels > :first-child { -+ color: black; -+ font-weight: 500; } -+ .login-dialog-auth-list-item-popup-box .login-dialog-auth-list-item-popup-labels > :last-child { -+ color: #222226; } ++ .login-dialog-item-icon-popup.popup-menu { ++ min-width: 0; } ++ .login-dialog-item-icon-popup-box .login-dialog-item-icon-popup-labels { ++ spacing: 3px; ++ text-align: center; } ++ .login-dialog-item-icon-popup-box .login-dialog-item-icon-popup-labels > :first-child { ++ color: black; ++ font-weight: 500; } ++ .login-dialog-item-icon-popup-box .login-dialog-item-icon-popup-labels > :last-child { ++ color: #222226; } .login-dialog-user-list-view { width: 25em; -@@ -3190,6 +3328,9 @@ StScrollBar { +@@ -3190,6 +3335,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%); } @@ -2559,7 +2603,7 @@ index 174d723..fa83485 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 +3342,102 @@ StScrollBar { +@@ -3201,6 +3349,102 @@ 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); } @@ -2662,7 +2706,7 @@ index 174d723..fa83485 100644 .unlock-dialog { background-color: transparent; } -@@ -3309,3 +3546,10 @@ StScrollBar { +@@ -3309,3 +3553,10 @@ StScrollBar { .login-dialog .user-widget.vertical .user-icon StIcon, .unlock-dialog .user-widget.vertical .user-icon StIcon { padding: 30px; } @@ -2674,5 +2718,5 @@ index 174d723..fa83485 100644 + border-color: #e9e9ea; + color: #222226; } -- -2.51.1 +2.53.0 diff --git a/0001-gdm-Work-around-failing-fingerprint-auth.patch b/0001-gdm-Work-around-failing-fingerprint-auth.patch index 5d73d00..30d2260 100644 --- a/0001-gdm-Work-around-failing-fingerprint-auth.patch +++ b/0001-gdm-Work-around-failing-fingerprint-auth.patch @@ -9,44 +9,70 @@ 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/util.js | 18 ++++++++++++++++++ - 1 file changed, 18 insertions(+) + js/gdm/authServicesLegacy.js | 44 +++++++++++++++++++++++++----------- + 1 file changed, 31 insertions(+), 13 deletions(-) -diff --git a/js/gdm/util.js b/js/gdm/util.js -index f6b797c321..aced46e78c 100644 ---- a/js/gdm/util.js -+++ b/js/gdm/util.js -@@ -109,6 +109,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { - this._defaultService = null; - this._preemptingService = null; - this._fingerprintReaderType = FingerprintReaderType.NONE; -+ this._fprintStartTime = -1; +diff --git a/js/gdm/authServicesLegacy.js b/js/gdm/authServicesLegacy.js +index 0813622a01..c85a9572f5 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()); - this._messageQueue = []; - this._messageQueueTimeoutId = 0; -@@ -670,6 +671,10 @@ export class ShellUserVerifier extends Signals.EventEmitter { - this._hold.acquire(); - try { - this._activeServices.add(serviceName); + this._fingerprintReadyTimeoutId = 0; ++ this._fprintStartTime = -1; + } + + _handleSelectChoice(serviceName, key) { +@@ -117,16 +118,20 @@ 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) { ++ this._fprintStartTime = GLib.get_monotonic_time(); + -+ if (serviceName == FINGERPRINT_SERVICE_NAME) -+ this._fprintStartTime = GLib.get_monotonic_time(); + - if (this._userName) { - await this._userVerifier.call_begin_verification_for_user( - serviceName, this._userName, this._cancellable); -@@ -764,6 +769,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { - const cancellable = this._cancellable; - this._fingerprintFailedId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, - FINGERPRINT_ERROR_TIMEOUT_WAIT, () => { -+ log("Generating _verificationFailed!"); - this._fingerprintFailedId = 0; - if (!cancellable.is_cancelled()) - this._verificationFailed(serviceName, false); -@@ -837,6 +843,18 @@ export class ShellUserVerifier extends Signals.EventEmitter { - if (serviceName === FINGERPRINT_SERVICE_NAME) { - if (this._fingerprintFailedId) - GLib.source_remove(this._fingerprintFailedId); ++ if (this._fingerprintReadyTimeoutId === 0) { ++ this._fingerprintReadyTimeoutId = GLib.timeout_add( ++ GLib.PRIORITY_DEFAULT, ++ FINGERPRINT_READY_TIMEOUT_MS, ++ () => { ++ this._fingerprintReadyTimeoutId = 0; ++ this._setFingerprintReady(); ++ return GLib.SOURCE_REMOVE; ++ }); ++ } + } + } + +@@ -234,6 +239,7 @@ export class AuthServicesLegacy extends AuthServices { + + this._fingerprintFailedId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, + FINGERPRINT_ERROR_TIMEOUT_WAIT, () => { ++ log("Generating _verificationFailed!"); + this._fingerprintFailedId = 0; + if (!this._cancellable.is_cancelled()) + this._verificationFailed(serviceName, false); +@@ -314,9 +320,21 @@ export class AuthServicesLegacy extends AuthServices { + + _handleVerificationFailed(serviceName) { + if (serviceName === Const.FINGERPRINT_SERVICE_NAME && +- this._enabledMechanisms.some(m => m.serviceName === serviceName) && +- this._fingerprintFailedId) +- GLib.source_remove(this._fingerprintFailedId); ++ this._enabledMechanisms.some(m => m.serviceName === serviceName)) { ++ if (this._fingerprintFailedId) ++ GLib.source_remove(this._fingerprintFailedId); + + // On Fedora we have the problem that fingerprint auth fails + // immediately if the PAM configuration has not been updated and no @@ -56,12 +82,12 @@ index f6b797c321..aced46e78c 100644 + if (this._fprintStartTime > GLib.get_monotonic_time() - GLib.USEC_PER_SEC) { + log("Fingerprint service failed almost immediately, considering it unavailable."); + log("Please fix your configuration by running: authselect select --force sssd with-fingerprint with-silent-lastlog"); -+ this._onServiceUnavailable(this._client, serviceName, null); -+ return; ++ this._onServiceUnavailable(serviceName, null); + } - } ++ } + } - // For Not Listed / enterprise logins, immediately reset + _handleOnVerificationComplete(serviceName) { -- -2.51.1 +2.53.0 diff --git a/gnome-shell.spec b/gnome-shell.spec index 1fa7dfb..097e0c7 100644 --- a/gnome-shell.spec +++ b/gnome-shell.spec @@ -33,10 +33,6 @@ Patch: 0001-build-Lower-gsettings-desktop-schemas-requirement.patch Patch: 0001-Revert-data-Drop-org.gnome.Shell.desktop.patch Patch: 0002-Reapply-main-Notify-gnome-session-when-we-re-ready.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 - # GDM/Lock stuff Patch: 0001-screenShield-unblank-when-inserting-smartcard.patch Patch: enforce-smartcard-at-unlock.patch @@ -45,6 +41,9 @@ Patch: 0001-main-Register-session-with-GDM-on-startup.patch # Passwordless work # https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/3212 Patch: 0001-Support-for-web-login-and-unified-auth-mechanism.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 # Extensions Patch: 0001-extensionDownloader-Refuse-to-override-system-extens.patch