diff --git a/SOURCES/0001-Support-conflicting-session-dialog.patch b/SOURCES/0001-Support-conflicting-session-dialog.patch index 2bc392a..b13ac47 100644 --- a/SOURCES/0001-Support-conflicting-session-dialog.patch +++ b/SOURCES/0001-Support-conflicting-session-dialog.patch @@ -369,15 +369,15 @@ index 25b86880d..83a3bb09d 100644 const ModalDialog = imports.ui.modalDialog; const PopupMenu = imports.ui.popupMenu; const Realmd = imports.gdm.realmd; -@@ -36,6 +37,7 @@ const UserWidget = imports.ui.userWidget; - const _FADE_ANIMATION_TIME = 250; +@@ -36,6 +38,7 @@ const _FADE_ANIMATION_TIME = 250; const _SCROLL_ANIMATION_TIME = 500; + const _FIXED_TOP_ACTOR_HEIGHT = 300; 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'); - var UserListItem = GObject.registerClass({ - Signals: { 'activate': {} }, -@@ -1044,6 +1046,22 @@ var LoginDialog = GObject.registerClass({ +@@ -1077,6 +1079,22 @@ var LoginDialog = GObject.registerClass({ }); } @@ -400,7 +400,7 @@ index 25b86880d..83a3bb09d 100644 _showConflictingSessionDialog(serviceName, conflictingSession) { let conflictingSessionDialog = new ConflictingSessionDialog(conflictingSession); -@@ -1063,6 +1081,17 @@ var LoginDialog = GObject.registerClass({ +@@ -1096,6 +1114,17 @@ var LoginDialog = GObject.registerClass({ } }, conflictingSessionDialog); @@ -418,7 +418,7 @@ index 25b86880d..83a3bb09d 100644 conflictingSessionDialog.open(); } -@@ -1318,6 +1347,9 @@ var LoginDialog = GObject.registerClass({ +@@ -1362,6 +1391,9 @@ var LoginDialog = GObject.registerClass({ this._updateCancelButton(); diff --git a/SOURCES/0001-Support-for-web-login-and-unified-auth-mechanism.patch b/SOURCES/0001-Support-for-web-login-and-unified-auth-mechanism.patch new file mode 100644 index 0000000..2af0bc9 --- /dev/null +++ b/SOURCES/0001-Support-for-web-login-and-unified-auth-mechanism.patch @@ -0,0 +1,8807 @@ +From 80b45dd1dbea84a6930c5a4a488b2cfdd28e62dd Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Thu, 2 Oct 2025 10:59:57 +0200 +Subject: [PATCH 01/48] style: Add common login dialog button styles to avoid + duplication + +This will be used in next commits, when new login buttons are added. + +Also, add a missing insensitive_button_bg_color on lockscren buttons, +without it the button was being dark when insensitive. +-- +2.51.0 + + +From f5323cccc3f0d454eed8e5a93b2faea78d783857 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Thu, 12 Feb 2026 16:37:08 +0100 +Subject: [PATCH 02/48] unlockDialog: Vertically center dialog using fixed + height + +Use a fixed estimated height for centering so the position stays +stable regardless of actual content height changes during user +interaction. + +Ensure clock is always on the same position adding vertically center alignment. +--- + js/ui/unlockDialog.js | 26 ++++++++++++++++++++------ + 1 file changed, 20 insertions(+), 6 deletions(-) + +diff --git a/js/ui/unlockDialog.js b/js/ui/unlockDialog.js +index 169043b2a6..3f8ddec651 100644 +--- a/js/ui/unlockDialog.js ++++ b/js/ui/unlockDialog.js +@@ -27,6 +27,8 @@ const BLUR_SIGMA = 60; + + const SUMMARY_ICON_SIZE = 32; + ++const FIXED_PROMPT_HEIGHT = 400; ++ + var NotificationsBox = GObject.registerClass({ + Signals: { 'wake-up-screen': {} }, + }, class NotificationsBox extends St.BoxLayout { +@@ -326,7 +328,11 @@ var NotificationsBox = GObject.registerClass({ + var Clock = GObject.registerClass( + class UnlockDialogClock extends St.BoxLayout { + _init() { +- super._init({ style_class: 'unlock-dialog-clock', vertical: true }); ++ super._init({ ++ style_class: 'unlock-dialog-clock', ++ orientation: Clutter.Orientation.VERTICAL, ++ y_align: Clutter.ActorAlign.CENTER, ++ }); + + this._time = new St.Label({ + style_class: 'unlock-dialog-clock-time', +@@ -417,8 +423,8 @@ class UnlockDialogLayout extends Clutter.LayoutManager { + 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 centerY = height / 2.0; + + let [, , stackWidth, stackHeight] = + this._stack.get_preferred_size(); +@@ -444,9 +450,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 (dialog._activePage === dialog._clock) { ++ stackY = Math.min( ++ Math.floor(centerY - stackHeight / 2.0), ++ height - stackHeight - maxNotificationsHeight); ++ } else { ++ stackY = Math.min( ++ Math.floor(centerY - FIXED_PROMPT_HEIGHT / 2.0), ++ height - stackHeight - maxNotificationsHeight); ++ } + + actorBox.x1 = columnX1; + actorBox.y1 = stackY; +-- +2.51.0 + + +From 5cc8c69402a35b9c14c1db1025db5b4f681a261d Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Thu, 12 Feb 2026 16:38:04 +0100 +Subject: [PATCH 03/48] unlockDialog: Fix username reuse on reset + +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 3f8ddec651..753c070bce 100644 +--- a/js/ui/unlockDialog.js ++++ b/js/ui/unlockDialog.js +@@ -802,7 +802,7 @@ var UnlockDialog = GObject.registerClass({ + + _onReset(authPrompt, beginRequest) { + let userName; +- if (beginRequest == AuthPrompt.BeginRequestType.PROVIDE_USERNAME) { ++ if (beginRequest !== AuthPrompt.BeginRequestType.DONT_PROVIDE_USERNAME) { + this._authPrompt.setUser(this._user); + userName = this._userName; + } else { +-- +2.51.0 + + +From 1a7adb6d93257b78e5fc8ab711fd781ed93a9dce Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Thu, 12 Feb 2026 16:38:54 +0100 +Subject: [PATCH 04/48] unlockDialog: Wait for authPrompt destruction before + 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 | 3 +-- + 1 file changed, 1 insertion(+), 2 deletions(-) + +diff --git a/js/ui/unlockDialog.js b/js/ui/unlockDialog.js +index 753c070bce..3bd40c92ac 100644 +--- a/js/ui/unlockDialog.js ++++ b/js/ui/unlockDialog.js +@@ -852,8 +852,7 @@ var UnlockDialog = GObject.registerClass({ + } + + _otherUserClicked() { +- Gdm.goto_login_session_sync(null); +- ++ this._authPrompt.connect('destroy', () => Gdm.goto_login_session_sync(null)); + this._authPrompt.cancel(); + } + +-- +2.51.0 + + +From 52e474166a0871545852ce63a1ca667d1f4ad9dd Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Tue, 10 Mar 2026 13:56:02 +0100 +Subject: [PATCH 05/48] authPrompt: Separate input well styles from prompt + layout + +Give _inputWell its own style class instead of reusing +login-dialog-prompt-layout. The prompt layout defines the width, +while the input well just fills horizontally within it. +-- +2.51.0 + + +From fc957c7f2c984cfbb48a648f7143e4de772f2489 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Thu, 5 Feb 2026 18:55:38 +0100 +Subject: [PATCH 06/48] 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 f9205d41dd..f048aa341e 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -285,7 +285,7 @@ var AuthPrompt = GObject.registerClass({ + + _activateNext(shouldSpin) { + this.verificationStatus = AuthPromptStatus.VERIFICATION_IN_PROGRESS; +- this.updateSensitivity(false); ++ this.updateSensitivity({sensitive: false}); + + if (this._queryingService) { + if (shouldSpin) +@@ -348,7 +348,7 @@ var AuthPrompt = GObject.registerClass({ + else + this.setQuestion(question.replace(/: *$/, '').trim()); + +- this.updateSensitivity(true); ++ this.updateSensitivity({sensitive: true}); + this.emit('prompted'); + } + +@@ -362,7 +362,7 @@ var AuthPrompt = GObject.registerClass({ + this._preemptiveAnswer = null; + + this.setChoiceList(promptMessage, choiceList); +- this.updateSensitivity(true); ++ this.updateSensitivity({sensitive: true}); + this.emit('prompted'); + } + +@@ -404,7 +404,7 @@ var AuthPrompt = GObject.registerClass({ + this.clear(); + } + +- this.updateSensitivity(canRetry); ++ this.updateSensitivity({sensitive: canRetry}); + this.setActorInDefaultButtonWell(null); + + if (!canRetry) +@@ -525,12 +525,12 @@ var 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}), + }); + } + +@@ -605,7 +605,7 @@ var AuthPrompt = GObject.registerClass({ + } + } + +- updateSensitivity(sensitive) { ++ updateSensitivity({sensitive}) { + let authWidget; + + if (this._authList.visible) +@@ -635,7 +635,7 @@ var AuthPrompt = GObject.registerClass({ + + this.setUser(null); + +- this.updateSensitivity(true); ++ this.updateSensitivity({sensitive: true}); + this._entry.set_text(''); + } + +@@ -728,7 +728,7 @@ var AuthPrompt = GObject.registerClass({ + params = Params.parse(params, { userName: null, + 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 8ed0a0d322..96999d7e6f 100644 +--- a/js/gdm/loginDialog.js ++++ b/js/gdm/loginDialog.js +@@ -947,8 +947,8 @@ var 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 }); +@@ -957,7 +957,7 @@ var 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 3bd40c92ac..19df7fc7b2 100644 +--- a/js/ui/unlockDialog.js ++++ b/js/ui/unlockDialog.js +@@ -719,7 +719,7 @@ var UnlockDialog = GObject.registerClass({ + case AuthPrompt.AuthPromptStatus.VERIFICATION_FAILED: + this._authPrompt.reset(); + this._authPrompt.updateSensitivity( +- verificationStatus === AuthPrompt.AuthPromptStatus.NOT_VERIFYING); ++ {sensitive: verificationStatus === AuthPrompt.AuthPromptStatus.NOT_VERIFYING}); + } + } + +-- +2.51.0 + + +From 326e650dddb72c20b69a29748cf05a5ee1205699 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Thu, 12 Feb 2026 16:47:22 +0100 +Subject: [PATCH 07/48] authPrompt: Use array-based widget lookup in + 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 f048aa341e..868d3a592d 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -606,12 +606,9 @@ var 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.51.0 + + +From c59a7bccea69a6e2ed9bbd5e8c858807523038cc Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Tue, 10 Mar 2026 17:10:58 +0100 +Subject: [PATCH 08/48] authPrompt: Generalize _fadeInChoiceList to accept any + element + +Rename to _fadeInElement and take the element as a parameter, +allowing the fade-in animation to be reused for other widgets. +Also skip the animation if the element is already visible. +--- + js/gdm/authPrompt.js | 11 +++++++---- + 1 file changed, 7 insertions(+), 4 deletions(-) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index 868d3a592d..b76326c490 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -520,13 +520,16 @@ var AuthPrompt = GObject.registerClass({ + this._entry.grab_key_focus(); + } + +- _fadeInChoiceList() { +- this._authList.set({ ++ _fadeInElement(element) { ++ if (element.visible) ++ return; ++ ++ element.set({ + opacity: 0, + visible: true, + }); + this.updateSensitivity({sensitive: false}); +- this._authList.ease({ ++ element.ease({ + opacity: 255, + duration: MESSAGE_FADE_OUT_ANIMATION_TIME, + transition: Clutter.AnimationMode.EASE_OUT_QUAD, +@@ -545,7 +548,7 @@ var AuthPrompt = GObject.registerClass({ + this._entry.hide(); + if (this._message.text === '') + this._message.hide(); +- this._fadeInChoiceList(); ++ this._fadeInElement(this._authList); + } + + getAnswer() { +-- +2.51.0 + + +From 1818770978fe025456807dbf0122f6faebc16f46 Mon Sep 17 00:00:00 2001 +From: Ray Strode +Date: Fri, 9 Feb 2024 09:02:25 -0500 +Subject: [PATCH 09/48] authPrompt: Fade out input buttons/entry after + verification + +It's nice to just see the user image and post login messages +once the user is done with the prompt. The buttons and +to some extent the password entry can disrupt the natural +login flow. +--- + js/gdm/authPrompt.js | 17 +++++++++++++++-- + js/gdm/loginDialog.js | 11 +++++++++++ + 2 files changed, 26 insertions(+), 2 deletions(-) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index b76326c490..613c594a94 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -50,6 +50,7 @@ var AuthPrompt = GObject.registerClass({ + 'next': {}, + 'prompted': {}, + 'reset': { param_types: [GObject.TYPE_UINT] }, ++ 'verification-complete': {}, + }, + }, class AuthPrompt extends St.BoxLayout { + _init(gdmClient, mode) { +@@ -417,8 +418,16 @@ var AuthPrompt = GObject.registerClass({ + _onVerificationComplete() { + this.setActorInDefaultButtonWell(null); + this.verificationStatus = AuthPromptStatus.VERIFICATION_SUCCEEDED; +- this.cancelButton.reactive = false; +- this.cancelButton.can_focus = false; ++ ++ this._mainBox.reactive = false; ++ this._mainBox.can_focus = false; ++ this._mainBox.ease({ ++ opacity: 0, ++ duration: MESSAGE_FADE_OUT_ANIMATION_TIME, ++ mode: Clutter.AnimationMode.EASE_OUT_QUAD, ++ }); ++ ++ this.emit('verification-complete'); + } + + _onReset() { +@@ -505,6 +514,10 @@ var AuthPrompt = GObject.registerClass({ + this.stopSpinning(); + this._authList.clear(); + this._authList.hide(); ++ ++ this._mainBox.opacity = 255; ++ this._mainBox.reactive = true; ++ this._mainBox.can_focus = true; + } + + setQuestion(question) { +diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js +index 96999d7e6f..bc62416e88 100644 +--- a/js/gdm/loginDialog.js ++++ b/js/gdm/loginDialog.js +@@ -451,6 +451,7 @@ var LoginDialog = GObject.registerClass({ + this._authPrompt = new AuthPrompt.AuthPrompt(this._gdmClient, AuthPrompt.AuthPromptMode.UNLOCK_OR_LOG_IN); + 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)); + this._authPrompt.hide(); + this.add_child(this._authPrompt); + +@@ -890,6 +891,16 @@ var LoginDialog = GObject.registerClass({ + } + } + ++ _onVerificationComplete() { ++ this._bottomButtonGroup.reactive = false; ++ this._bottomButtonGroup.can_focus = false; ++ this._bottomButtonGroup.ease({ ++ opacity: 0, ++ duration: _FADE_ANIMATION_TIME, ++ mode: Clutter.AnimationMode.EASE_OUT_QUAD, ++ }); ++ } ++ + _onDefaultSessionChanged(client, sessionId) { + this._sessionMenuButton.setActiveSession(sessionId); + } +-- +2.51.0 + + +From dcbfeceb6ec5cb6be19530e303908a00018f5e3d Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Wed, 21 Jan 2026 14:23:34 +0100 +Subject: [PATCH 10/48] authPrompt: Don't reset preemptiveAnswer when + VERIFICATION_IN_PROGRESS + +PreemptiveAnswer wasn't being used in the case where verification is in +progress and the smartcard is inserted, triggering a reset. + +This change ensures a preemptive answer will be used once smartcard +service asks for the PIN. +--- + js/gdm/authPrompt.js | 3 ++- + 1 file changed, 2 insertions(+), 1 deletion(-) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index 613c594a94..1b6f67035a 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -682,7 +682,8 @@ var AuthPrompt = GObject.registerClass({ + this.verificationStatus = AuthPromptStatus.NOT_VERIFYING; + this.cancelButton.reactive = this._hasCancelButton; + this.cancelButton.can_focus = this._hasCancelButton; +- this._preemptiveAnswer = null; ++ if (oldStatus !== AuthPromptStatus.VERIFICATION_IN_PROGRESS) ++ this._preemptiveAnswer = null; + + if (this._preemptiveAnswerWatchId) + this._idleMonitor.remove_watch(this._preemptiveAnswerWatchId); +-- +2.51.0 + + +From 74d754e9ce157599449d73838688aab698d257e9 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Thu, 12 Feb 2026 17:05:32 +0100 +Subject: [PATCH 11/48] style: Increase hint-text left margin + +The cursor was overlapping the hint-text, making it difficult to read. +Increase the left margin to ensure proper readability. +--- + data/theme/gnome-shell-sass/widgets/_entries.scss | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/data/theme/gnome-shell-sass/widgets/_entries.scss b/data/theme/gnome-shell-sass/widgets/_entries.scss +index 0a43e86f37..d76d17f282 100644 +--- a/data/theme/gnome-shell-sass/widgets/_entries.scss ++++ b/data/theme/gnome-shell-sass/widgets/_entries.scss +@@ -21,7 +21,7 @@ StEntry { + padding: 0 4px; + } + StLabel.hint-text { +- margin-left: 2px; ++ margin-left: $base_margin * 2; + color: transparentize($fg_color, 0.3); + } + } +-- +2.51.0 + + +From a3e17c6c7e0fc5af4232e871fd8c9011b2baf190 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Mon, 16 Feb 2026 16:05:17 +0100 +Subject: [PATCH 12/48] authPrompt: Use connectObject for userVerifier signals + +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 1b6f67035a..c26b98c024 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -78,14 +78,16 @@ var AuthPrompt = GObject.registerClass({ + + this._userVerifier = new GdmUtil.ShellUserVerifier(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)); +@@ -132,6 +134,7 @@ var AuthPrompt = GObject.registerClass({ + this._preemptiveAnswerWatchId = 0; + } + ++ this._userVerifier.disconnectObject(this); + this._userVerifier.destroy(); + this._userVerifier = null; + this._entry = null; +-- +2.51.0 + + +From 21e361883a579b235b9076e774156bd42ea888ac Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Thu, 12 Feb 2026 17:09:44 +0100 +Subject: [PATCH 13/48] authPrompt: Group animation constants together + +Move all animation-related constants together near the top of the +file to follow the style of the rest of the codebase. +--- + js/gdm/authPrompt.js | 1 - + 1 file changed, 1 deletion(-) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index c26b98c024..fa0fb8f5af 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -17,7 +17,6 @@ const Util = imports.misc.util; + var DEFAULT_BUTTON_WELL_ICON_SIZE = 16; + var DEFAULT_BUTTON_WELL_ANIMATION_DELAY = 1000; + var DEFAULT_BUTTON_WELL_ANIMATION_TIME = 300; +- + var MESSAGE_FADE_OUT_ANIMATION_TIME = 500; + + const LOCKDOWN_SCHEMA = 'org.gnome.desktop.lockdown'; +-- +2.51.0 + + +From db22c1b01dc865aef97c79d2558aac09285de678 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Thu, 12 Feb 2026 17:20:34 +0100 +Subject: [PATCH 14/48] authPrompt: Update entry layout based on mockups + +Implement the new authentication prompt layout based on the design +mockups at: +https://gitlab.gnome.org/Teams/Design/os-mockups/-/blob/master/lock-login/passwordless-login-aday.png + +The entry is now larger and more rounded. The entry area has a richer +UI with the password visibility toggle (already present) and a new +_defaultButtonWell inside it that can show either a next button +(default) or a spinner when authentication is in progress. + +To achieve this, _entry and _defaultButtonWell are wrapped in a new +_entryArea container with a BinLayout, allowing _defaultButtonWell +to overlay _entry aligned to the right. + +The _mainBox now uses a BinLayout to allow it to be horizontally centered +below the username and avatar. This also enables constraining +the cancelButton to the left edge of _mainBox using an AlignConstraint +on the X axis, so it appears offset to the left without affecting +the horizontal center of _mainBox. +--- + js/gdm/authPrompt.js | 69 ++++++++++++++++++++++++++++++++------------ + 1 file changed, 51 insertions(+), 18 deletions(-) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index fa0fb8f5af..7e9469391d 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -1,7 +1,7 @@ + // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + /* exported AuthPrompt */ + +-const { Clutter, Gio, GLib, GObject, Meta, Pango, Shell, St } = imports.gi; ++const { Clutter, Gio, GLib, GObject, Graphene, Meta, Pango, Shell, St } = imports.gi; + + const Animation = imports.ui.animation; + const AuthList = imports.gdm.authList; +@@ -146,9 +146,11 @@ var AuthPrompt = GObject.registerClass({ + } + + _initInputRow() { +- this._mainBox = new St.BoxLayout({ ++ this._mainBox = new St.Widget({ ++ layout_manager: new Clutter.BinLayout(), + style_class: 'login-dialog-button-box', +- vertical: false, ++ x_expand: true, ++ y_expand: false, + }); + this.add_child(this._mainBox); + +@@ -158,10 +160,17 @@ var AuthPrompt = GObject.registerClass({ + button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE, + reactive: this._hasCancelButton, + can_focus: this._hasCancelButton, ++ x_expand: true, + x_align: Clutter.ActorAlign.START, + y_align: Clutter.ActorAlign.CENTER, + child: new St.Icon({ icon_name: 'go-previous-symbolic' }), + }); ++ this.cancelButton.add_constraint(new Clutter.AlignConstraint({ ++ source: this._mainBox, ++ align_axis: Clutter.AlignAxis.X_AXIS, ++ pivot_point: new Graphene.Point({x: 1, y: 0}), ++ })); ++ + if (this._hasCancelButton) + this.cancelButton.connect('clicked', () => this.cancel()); + else +@@ -187,10 +196,20 @@ var AuthPrompt = GObject.registerClass({ + }); + this._mainBox.add_child(this._authList); + +- let entryParams = { ++ this._entryArea = new St.Widget({ ++ style_class: 'login-dialog-prompt-entry-area', ++ layout_manager: new Clutter.BinLayout(), ++ x_expand: true, ++ y_expand: true, ++ visible: false, ++ }); ++ this._mainBox.add_child(this._entryArea); ++ ++ const entryParams = { + style_class: 'login-dialog-prompt-entry', + can_focus: true, + x_expand: true, ++ y_expand: true, + }; + + this._entry = null; +@@ -202,8 +221,7 @@ var AuthPrompt = GObject.registerClass({ + ShellEntry.addContextMenu(this._passwordEntry, { actionMode: Shell.ActionMode.NONE }); + + this._entry = this._passwordEntry; +- this._mainBox.add_child(this._entry); +- this._entry.grab_key_focus(); ++ this._entryArea.add_child(this._entry); + + this._lockdownSettings = new Gio.Settings({ schema_id: LOCKDOWN_SCHEMA }); + this._lockdownSettings.connect(`changed::${DISABLE_SHOW_PASSWORD_KEY}`, +@@ -232,17 +250,28 @@ var AuthPrompt = GObject.registerClass({ + + this._defaultButtonWell = new St.Widget({ + layout_manager: new Clutter.BinLayout(), ++ style_class: 'login-dialog-default-button-well', ++ x_expand: true, + x_align: Clutter.ActorAlign.END, + y_align: Clutter.ActorAlign.CENTER, + }); +- this._defaultButtonWell.add_constraint(new Clutter.BindConstraint({ +- source: this.cancelButton, +- coordinate: Clutter.BindCoordinate.WIDTH, +- })); +- this._mainBox.add_child(this._defaultButtonWell); ++ this._entryArea.add_child(this._defaultButtonWell); ++ ++ this._nextButton = new St.Button({ ++ style_class: 'login-dialog-button next-button', ++ button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE, ++ reactive: true, ++ can_focus: false, ++ icon_name: 'go-next-symbolic', ++ }); ++ this._nextButton.connect('clicked', () => this._activateNext()); ++ this._nextButton.add_style_pseudo_class('default'); ++ this._defaultButtonWell.add_child(this._nextButton); + + this._spinner = new Animation.Spinner(DEFAULT_BUTTON_WELL_ICON_SIZE); + this._defaultButtonWell.add_child(this._spinner); ++ ++ this.setActorInDefaultButtonWell(this._nextButton); + } + + _updateShowPasswordIcon() { +@@ -319,7 +348,7 @@ var AuthPrompt = GObject.registerClass({ + } + + if (newEntry) { +- this._mainBox.replace_child(this._entry, newEntry); ++ this._entryArea.replace_child(this._entry, newEntry); + this._entry = newEntry; + this._inactiveEntry = inactiveEntry; + +@@ -408,17 +437,17 @@ var AuthPrompt = GObject.registerClass({ + } + + this.updateSensitivity({sensitive: canRetry}); +- this.setActorInDefaultButtonWell(null); ++ this.setActorInDefaultButtonWell(this._nextButton); + + if (!canRetry) + this.verificationStatus = AuthPromptStatus.VERIFICATION_FAILED; + + if (wasQueryingService) +- Util.wiggle(this._entry); ++ Util.wiggle(this._entryArea); + } + + _onVerificationComplete() { +- this.setActorInDefaultButtonWell(null); ++ this.setActorInDefaultButtonWell(this._nextButton, true); + this.verificationStatus = AuthPromptStatus.VERIFICATION_SUCCEEDED; + + this._mainBox.reactive = false; +@@ -531,7 +560,8 @@ var AuthPrompt = GObject.registerClass({ + this._entry.hint_text = question; + + this._authList.hide(); +- this._entry.show(); ++ ++ this._entryArea.show(); + this._entry.grab_key_focus(); + } + +@@ -560,7 +590,7 @@ var 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); +@@ -631,6 +661,9 @@ var AuthPrompt = GObject.registerClass({ + if (authWidget.reactive === sensitive) + return; + ++ if (authWidget === this._entry) ++ this._nextButton.reactive = sensitive; ++ + authWidget.reactive = sensitive; + + if (sensitive) { +@@ -644,7 +677,7 @@ var AuthPrompt = GObject.registerClass({ + } + + vfunc_hide() { +- this.setActorInDefaultButtonWell(null, true); ++ this.setActorInDefaultButtonWell(this._nextButton, true); + super.vfunc_hide(); + this._message.opacity = 0; + +-- +2.51.0 + + +From fc95866bf3faefd93e5b323876a6bb509dc7d54d Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Thu, 12 Feb 2026 17:41:48 +0100 +Subject: [PATCH 15/48] authPrompt: Fade in _entryArea instead of abruptly + showing it + +To support the new fade-in flow, the _entryArea is now hidden by default. +The clear() function has been updated to reset this state to hidden. + +In _onVerificationFailed(), the call to clear() was removed to prevent the +area from being hidden prematurely. This is safe because a reset() (which +calls clear()) is triggered later in the process. + +_fadeInElement() will call updateSensitivity() to ensure the widget is +sensitive when the animation finishes, so there's no need to call it +explicitly in _onAskQuestion(). +--- + js/gdm/authPrompt.js | 9 +++------ + 1 file changed, 3 insertions(+), 6 deletions(-) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index 7e9469391d..9503030d3b 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -380,7 +380,6 @@ var AuthPrompt = GObject.registerClass({ + else + this.setQuestion(question.replace(/: *$/, '').trim()); + +- this.updateSensitivity({sensitive: true}); + this.emit('prompted'); + } + +@@ -431,10 +430,8 @@ var 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); +@@ -540,6 +537,7 @@ var AuthPrompt = GObject.registerClass({ + } + + clear() { ++ this._entryArea.hide(); + this._entry.text = ''; + this._inactiveEntry.text = ''; + this.stopSpinning(); +@@ -561,8 +559,7 @@ var AuthPrompt = GObject.registerClass({ + + this._authList.hide(); + +- this._entryArea.show(); +- this._entry.grab_key_focus(); ++ this._fadeInElement(this._entryArea); + } + + _fadeInElement(element) { +-- +2.51.0 + + +From 64c603f3e601c271ddd32186943186c5a9f52bc7 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Thu, 12 Feb 2026 17:36:08 +0100 +Subject: [PATCH 16/48] authPrompt: Show entry area when displaying message + +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 | 9 +++++++++ + 1 file changed, 9 insertions(+) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index 9503030d3b..2467362fe4 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -424,6 +424,15 @@ var AuthPrompt = GObject.registerClass({ + + _onShowMessage(_userVerifier, serviceName, message, type) { + this.setMessage(serviceName, message, type); ++ ++ // If we're showing a message and no auth widget is currently visible, ++ // show the entry area to allow getting a preemptive answer ++ if (message && ++ type < GdmUtil.MessageType.ERROR && ++ !this._entryArea.visible && ++ !this._authList.visible) ++ this._fadeInElement(this._entryArea); ++ + this.emit('prompted'); + } + +-- +2.51.0 + + +From 88bbd0ee5ae2ce1b47a89d55fa9870145908c8ac Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Thu, 12 Feb 2026 17:28:54 +0100 +Subject: [PATCH 17/48] authPrompt: Refactor button-well animation and add + loading signal + +- Add start/stop spinning() methods. +- Add 'loading' signal emitted on start/stop spinning (used in upcoming + commits). +- Replace setActorInDefaultButtonWell() with start/stop spinning() + methods. +- Simplify _activateNext checking internally reactive state and + passwordEntry. +- Simplify setActorInDefaultButtonWell and remove unused + DEFAULT_BUTTON_WELL_ANIMATION_DELAY constant. +--- + js/gdm/authPrompt.js | 113 +++++++++++++++++++------------------------ + 1 file changed, 50 insertions(+), 63 deletions(-) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index 2467362fe4..370a0601e8 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -15,7 +15,6 @@ const UserWidget = imports.ui.userWidget; + const Util = imports.misc.util; + + var DEFAULT_BUTTON_WELL_ICON_SIZE = 16; +-var DEFAULT_BUTTON_WELL_ANIMATION_DELAY = 1000; + var DEFAULT_BUTTON_WELL_ANIMATION_TIME = 300; + var MESSAGE_FADE_OUT_ANIMATION_TIME = 500; + +@@ -50,6 +49,7 @@ var AuthPrompt = GObject.registerClass({ + 'prompted': {}, + 'reset': { param_types: [GObject.TYPE_UINT] }, + 'verification-complete': {}, ++ 'loading': {param_types: [GObject.TYPE_BOOLEAN]}, + }, + }, class AuthPrompt extends St.BoxLayout { + _init(gdmClient, mode) { +@@ -241,11 +241,7 @@ var AuthPrompt = GObject.registerClass({ + 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({ +@@ -315,13 +311,16 @@ var 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) +- this.startSpinning(); ++ if (this._entry === this._passwordEntry) ++ this.startSpinning({animate: true}); + + this._userVerifier.answerQuery(this._queryingService, this._entry.text); + } else { +@@ -443,7 +442,7 @@ var AuthPrompt = GObject.registerClass({ + this._queryingService = null; + + this.updateSensitivity({sensitive: canRetry}); +- this.setActorInDefaultButtonWell(this._nextButton); ++ this.stopSpinning(); + + if (!canRetry) + this.verificationStatus = AuthPromptStatus.VERIFICATION_FAILED; +@@ -453,7 +452,7 @@ var AuthPrompt = GObject.registerClass({ + } + + _onVerificationComplete() { +- this.setActorInDefaultButtonWell(this._nextButton, true); ++ this.stopSpinning({animate: true}); + this.verificationStatus = AuthPromptStatus.VERIFICATION_SUCCEEDED; + + this._mainBox.reactive = false; +@@ -473,76 +472,64 @@ var AuthPrompt = GObject.registerClass({ + } + + setActorInDefaultButtonWell(actor, animate) { +- if (!this._defaultButtonWellActor && +- !actor) ++ if (!this._defaultButtonWellActor && !actor) + return; + +- let oldActor = this._defaultButtonWellActor; ++ const oldActor = this._defaultButtonWellActor; ++ const wasSpinner = oldActor === this._spinner; + + if (oldActor) + oldActor.remove_all_transitions(); + +- let wasSpinner; +- if (oldActor == this._spinner) +- wasSpinner = true; +- else +- wasSpinner = false; ++ if (actor === this._spinner) ++ this._spinner.play(); + +- let isSpinner; +- if (actor == this._spinner) +- isSpinner = true; +- else +- isSpinner = false; +- +- if (this._defaultButtonWellActor != actor && oldActor) { +- if (!animate) { ++ if (!animate) { ++ if (oldActor) { + oldActor.opacity = 0; +- +- if (wasSpinner) { +- if (this._spinner) +- this._spinner.stop(); +- } +- } else { +- oldActor.ease({ +- opacity: 0, +- duration: DEFAULT_BUTTON_WELL_ANIMATION_TIME, +- delay: DEFAULT_BUTTON_WELL_ANIMATION_DELAY, +- mode: Clutter.AnimationMode.LINEAR, +- onComplete: () => { +- if (wasSpinner) { +- if (this._spinner) +- this._spinner.stop(); +- } +- }, +- }); ++ if (wasSpinner) ++ this._spinner.stop(); + } ++ if (actor) ++ actor.opacity = 255; ++ ++ this._defaultButtonWellActor = actor; ++ return; + } + ++ if (oldActor) { ++ oldActor.opacity = 255; ++ oldActor.ease({ ++ opacity: 0, ++ duration: DEFAULT_BUTTON_WELL_ANIMATION_TIME, ++ mode: Clutter.AnimationMode.LINEAR, ++ onComplete: () => { ++ if (wasSpinner) ++ this._spinner.stop(); ++ }, ++ }); ++ } + if (actor) { +- if (isSpinner) +- this._spinner.play(); +- +- if (!animate) { +- actor.opacity = 255; +- } else { +- actor.ease({ +- opacity: 255, +- duration: DEFAULT_BUTTON_WELL_ANIMATION_TIME, +- delay: DEFAULT_BUTTON_WELL_ANIMATION_DELAY, +- mode: Clutter.AnimationMode.LINEAR, +- }); +- } ++ actor.opacity = 0; ++ actor.ease({ ++ opacity: 255, ++ duration: DEFAULT_BUTTON_WELL_ANIMATION_TIME, ++ delay: oldActor ? DEFAULT_BUTTON_WELL_ANIMATION_TIME : 0, ++ mode: Clutter.AnimationMode.LINEAR, ++ }); + } + + this._defaultButtonWellActor = actor; + } + +- startSpinning() { +- this.setActorInDefaultButtonWell(this._spinner, true); ++ startSpinning({animate = false} = {}) { ++ this.emit('loading', true); ++ this.setActorInDefaultButtonWell(this._spinner, animate); + } + +- stopSpinning() { +- this.setActorInDefaultButtonWell(null, false); ++ stopSpinning({animate = false} = {}) { ++ this.emit('loading', false); ++ this.setActorInDefaultButtonWell(this._nextButton, animate); + } + + clear() { +@@ -683,7 +670,7 @@ var AuthPrompt = GObject.registerClass({ + } + + vfunc_hide() { +- this.setActorInDefaultButtonWell(this._nextButton, true); ++ this.stopSpinning(); + super.vfunc_hide(); + this._message.opacity = 0; + +-- +2.51.0 + + +From bcff8a5091b13cd0f0e2555ef52e27b3a028cb62 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Tue, 10 Mar 2026 18:32:48 +0100 +Subject: [PATCH 18/48] authPrompt: Skip reset after successful verification + +Don't reset the prompt if verification has already succeeded. +--- + js/gdm/authPrompt.js | 3 +++ + 1 file changed, 3 insertions(+) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index 370a0601e8..a970f17ca9 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -467,6 +467,9 @@ var AuthPrompt = GObject.registerClass({ + } + + _onReset() { ++ if (this.verificationStatus === AuthPromptStatus.VERIFICATION_SUCCEEDED) ++ return; ++ + this.verificationStatus = AuthPromptStatus.NOT_VERIFYING; + this.reset(); + } +-- +2.51.0 + + +From 26aef7076824f3e50c1b3ffb9e7f3141f1ac826e Mon Sep 17 00:00:00 2001 +From: Ray Strode +Date: Tue, 6 Feb 2024 13:06:32 -0500 +Subject: [PATCH 19/48] authPrompt: Parameterize reset function + +In the future, userVerifier will request a partial reset where +some state is carried over or explicitly specified. + +This commit prepares for that by allowing the request type and +reusing entry text to be specified at reset time. +--- + js/gdm/authPrompt.js | 32 ++++++++++++++++++++++---------- + 1 file changed, 22 insertions(+), 10 deletions(-) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index a970f17ca9..80c65eef90 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -466,12 +466,11 @@ var AuthPrompt = GObject.registerClass({ + this.emit('verification-complete'); + } + +- _onReset() { ++ _onReset(_userVerifier, resetParams) { + if (this.verificationStatus === AuthPromptStatus.VERIFICATION_SUCCEEDED) + return; + +- this.verificationStatus = AuthPromptStatus.NOT_VERIFYING; +- this.reset(); ++ this.reset(resetParams); + } + + setActorInDefaultButtonWell(actor, animate) { +@@ -535,10 +534,17 @@ var 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(); +@@ -708,8 +714,13 @@ var AuthPrompt = GObject.registerClass({ + this.updateSensitivity(false); + } + +- reset() { +- let oldStatus = this.verificationStatus; ++ reset(params) { ++ const {reuseEntryText, softReset} = Params.parse(params, { ++ reuseEntryText: false, ++ softReset: false, ++ }); ++ ++ const oldStatus = this.verificationStatus; + this.verificationStatus = AuthPromptStatus.NOT_VERIFYING; + this.cancelButton.reactive = this._hasCancelButton; + this.cancelButton.can_focus = this._hasCancelButton; +@@ -725,7 +736,7 @@ var AuthPrompt = GObject.registerClass({ + this._userVerifier.cancel(); + + this._queryingService = null; +- this.clear(); ++ this.clear({reuseEntryText}); + this._message.opacity = 0; + this.setUser(null); + this._updateEntry(true); +@@ -750,7 +761,8 @@ var AuthPrompt = GObject.registerClass({ + // We don't need to know the username if the user preempted the login screen + // with a smartcard or with preauthenticated oVirt credentials + beginRequestType = BeginRequestType.DONT_PROVIDE_USERNAME; +- } else if (oldStatus === AuthPromptStatus.VERIFICATION_IN_PROGRESS) { ++ } else if (oldStatus === AuthPromptStatus.VERIFICATION_IN_PROGRESS || ++ softReset) { + // We're going back to retry with current user + beginRequestType = BeginRequestType.REUSE_USERNAME; + } else { +-- +2.51.0 + + +From 17de39832ff7a953840f898a8bac829ab6f560e6 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Thu, 12 Feb 2026 16:56:48 +0100 +Subject: [PATCH 20/48] authPrompt: Rename BeginRequestType to ResetType + +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 | 15 ++++++++------- + js/gdm/loginDialog.js | 6 +++--- + js/ui/unlockDialog.js | 4 ++-- + 3 files changed, 13 insertions(+), 12 deletions(-) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index 80c65eef90..15e82fc212 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -35,7 +35,8 @@ var AuthPromptStatus = { + VERIFICATION_IN_PROGRESS: 5, + }; + +-var BeginRequestType = { ++/** @enum {number} */ ++var ResetType = { + PROVIDE_USERNAME: 0, + DONT_PROVIDE_USERNAME: 1, + REUSE_USERNAME: 2, +@@ -747,30 +748,30 @@ var AuthPrompt = GObject.registerClass({ + 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.serviceIsForeground(OVirt.SERVICE_NAME) || + this._userVerifier.serviceIsForeground(Vmware.SERVICE_NAME) || + this._userVerifier.serviceIsForeground(GdmUtil.SMARTCARD_SERVICE_NAME)) { + // 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 bc62416e88..3f3d8702cb 100644 +--- a/js/gdm/loginDialog.js ++++ b/js/gdm/loginDialog.js +@@ -865,7 +865,7 @@ var LoginDialog = GObject.registerClass({ + } + } + +- _onReset(authPrompt, beginRequest) { ++ _onReset(authPrompt, resetType) { + this._resetGreeterProxy(); + this._sessionMenuButton.updateSensitivity(true); + +@@ -877,11 +877,11 @@ var 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 19df7fc7b2..ba32d45d5d 100644 +--- a/js/ui/unlockDialog.js ++++ b/js/ui/unlockDialog.js +@@ -800,9 +800,9 @@ var 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.51.0 + + +From 7780db5e45adadb74998adf5937badd9c601a4b2 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Wed, 11 Feb 2026 16:11:59 +0100 +Subject: [PATCH 21/48] unlockDialog: Use isprint() instead of isgraph() for + 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 ba32d45d5d..941bcf7b51 100644 +--- a/js/ui/unlockDialog.js ++++ b/js/ui/unlockDialog.js +@@ -647,7 +647,7 @@ var UnlockDialog = GObject.registerClass({ + + this._showPrompt(); + +- if (GLib.unichar_isgraph(unichar)) ++ if (GLib.unichar_isprint(unichar)) + this._authPrompt.addCharacter(unichar); + + return Clutter.EVENT_PROPAGATE; +-- +2.51.0 + + +From 47dfb8390cb132ece1832b17754aaf56808111e5 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Wed, 21 Jan 2026 14:34:11 +0100 +Subject: [PATCH 22/48] authPrompt: Capture preemptive input before entry is + sensitive + +The previous changes made the entry invisible and insensitive until +askQuestion is called. During the lock screen, when the user starts +typing before the PAM service is ready, keystrokes were being lost. + +Replace addCharacter() with startPreemptiveInput() which buffers +keystrokes and handles Enter key presses while waiting for the entry +to become sensitive. + +Also, remove unsused addCharacter() in loginDialog.js. +--- + js/gdm/authPrompt.js | 43 +++++++++++++++++++++++++++++++++++-------- + js/gdm/loginDialog.js | 4 ---- + js/ui/unlockDialog.js | 2 +- + 3 files changed, 36 insertions(+), 13 deletions(-) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index 15e82fc212..8f5751ec99 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -143,6 +143,10 @@ var AuthPrompt = GObject.registerClass({ + vfunc_key_press_event(keyPressEvent) { + if (keyPressEvent.keyval == Clutter.KEY_Escape) + this.cancel(); ++ ++ if (this._preemptiveInput && !this._pendingActivate) ++ return this._entry.clutter_text.event(keyPressEvent, false); ++ + return super.vfunc_key_press_event(keyPressEvent); + } + +@@ -242,7 +246,16 @@ var AuthPrompt = GObject.registerClass({ + this._fadeOutMessage(); + }); + +- entry.clutter_text.connect('activate', () => this._activateNext()); ++ entry.clutter_text.connect('activate', () => { ++ if (this._preemptiveInput && ++ !this._pendingActivate && ++ this._entry.clutter_text.text) { ++ this._pendingActivate = true; ++ return; ++ } ++ ++ this._activateNext(); ++ }); + }); + + this._defaultButtonWell = new St.Widget({ +@@ -581,10 +594,24 @@ var AuthPrompt = GObject.registerClass({ + opacity: 255, + duration: MESSAGE_FADE_OUT_ANIMATION_TIME, + transition: Clutter.AnimationMode.EASE_OUT_QUAD, +- onComplete: () => this.updateSensitivity({sensitive: true}), ++ onComplete: () => { ++ this.updateSensitivity({sensitive: true}); ++ this._completePreemptiveInput({canActivate: element === this._entryArea}); ++ }, ++ onStopped: (isFinished) => { ++ if (!isFinished) ++ this._completePreemptiveInput(); ++ }, + }); + } + ++ _completePreemptiveInput({canActivate = false} = {}) { ++ if (this._pendingActivate && canActivate) ++ this._activateNext(); ++ this._preemptiveInput = false; ++ this._pendingActivate = false; ++ } ++ + setChoiceList(promptMessage, choiceList) { + this._authList.clear(); + this._authList.label.text = promptMessage; +@@ -716,7 +743,7 @@ var AuthPrompt = GObject.registerClass({ + } + + reset(params) { +- const {reuseEntryText, softReset} = Params.parse(params, { ++ let {reuseEntryText, softReset} = Params.parse(params, { + reuseEntryText: false, + softReset: false, + }); +@@ -736,6 +763,8 @@ var AuthPrompt = GObject.registerClass({ + if (this._userVerifier) + this._userVerifier.cancel(); + ++ reuseEntryText = reuseEntryText || this._preemptiveInput; ++ + this._queryingService = null; + this.clear({reuseEntryText}); + this._message.opacity = 0; +@@ -774,12 +803,10 @@ var AuthPrompt = GObject.registerClass({ + this.emit('reset', resetType); + } + +- addCharacter(unichar) { +- if (!this._entry.visible) +- return; +- +- this._entry.grab_key_focus(); ++ startPreemptiveInput(unichar) { ++ this._preemptiveInput = true; + this._entry.clutter_text.insert_unichar(unichar); ++ this.grab_key_focus(); + } + + begin(params) { +diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js +index 3f3d8702cb..36a727f01f 100644 +--- a/js/gdm/loginDialog.js ++++ b/js/gdm/loginDialog.js +@@ -1329,10 +1329,6 @@ var 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 941bcf7b51..07b2acaf45 100644 +--- a/js/ui/unlockDialog.js ++++ b/js/ui/unlockDialog.js +@@ -648,7 +648,7 @@ var UnlockDialog = GObject.registerClass({ + this._showPrompt(); + + if (GLib.unichar_isprint(unichar)) +- this._authPrompt.addCharacter(unichar); ++ this._authPrompt.startPreemptiveInput(unichar); + + return Clutter.EVENT_PROPAGATE; + } +-- +2.51.0 + + +From d04618600781d432113ce68b4fed56fbb6f06878 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Tue, 10 Feb 2026 18:51:41 +0100 +Subject: [PATCH 23/48] authPrompt: On verificationFailed ensure input + sensitivity is disabled + +There's a time window between the verification failing and a new +verification request being started. + +Input is not accepted during this time, so disable input sensitivity +when verification fails. UserVerifier will be in charge of restarting the +verification process, reenabling sensitivity. +--- + js/gdm/authPrompt.js | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index 8f5751ec99..48f2c8cdd6 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -455,7 +455,7 @@ var AuthPrompt = GObject.registerClass({ + if (wasQueryingService) + this._queryingService = null; + +- this.updateSensitivity({sensitive: canRetry}); ++ this.updateSensitivity({sensitive: false}); + this.stopSpinning(); + + if (!canRetry) +-- +2.51.0 + + +From e20104b165e40b34282b1559a046061d55327513 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Tue, 30 Sep 2025 17:43:22 +0200 +Subject: [PATCH 24/48] authPrompt: Update authList style + +Make the AuthListItem buttons a bit bigger and more rounded. + +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. + +Use accessible_name. + +Register classes with the new style. +--- + js/gdm/authList.js | 169 +++++++++++++++++++++++++++++++++++-------- + js/gdm/authPrompt.js | 30 ++++++-- + js/gdm/util.js | 6 +- + 3 files changed, 167 insertions(+), 38 deletions(-) + +diff --git a/js/gdm/authList.js b/js/gdm/authList.js +index fb223a9727..3f1b2817f9 100644 +--- a/js/gdm/authList.js ++++ b/js/gdm/authList.js +@@ -17,29 +17,134 @@ + */ + /* exported AuthList */ + +-const { Clutter, GObject, Meta, St } = imports.gi; ++const { Clutter, Graphene, Shell, GObject, Meta, St } = imports.gi; ++ ++const Main = imports.ui.main; ++const PopupMenu = imports.ui.popupMenu; + + const SCROLL_ANIMATION_TIME = 500; + +-const AuthListItem = GObject.registerClass({ +- Signals: { 'activate': {} }, +-}, class AuthListItem extends St.Button { +- _init(key, text) { +- this.key = key; +- const label = new St.Label({ +- text, +- style_class: 'login-dialog-auth-list-label', +- y_align: Clutter.ActorAlign.CENTER, +- x_expand: false, ++const ItemIconPopup = class extends PopupMenu.PopupMenu { ++ constructor(sourceActor, title, subtitle) { ++ super(sourceActor, 0.5, St.Side.TOP); ++ ++ 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, ++ 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 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.registerClass(this); ++ } ++ ++ constructor(iconName, iconTitle, iconSubtitle) { ++ super({ ++ style_class: 'login-dialog-item-icon', ++ iconName, ++ }); ++ ++ this._popup = new ItemIconPopup(this, iconTitle, iconSubtitle); ++ Main.uiGroup.add_child(this._popup.actor); ++ } ++} ++ ++class AuthListItem extends St.Button { ++ static [GObject.signals] = { ++ 'activate': {}, ++ }; ++ ++ static { ++ GObject.registerClass(this); ++ } + +- 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, + reactive: true, ++ accessible_name: [title, subtitle, iconTitle, iconSubtitle] ++ .filter(p => p) ++ .join(', '), ++ }); ++ ++ this.key = key; ++ ++ this._container = new St.Widget({ ++ layout_manager: new Clutter.BinLayout(), ++ x_expand: true, + }); ++ this._labelBox = new St.BoxLayout({ ++ orientation: Clutter.Orientation.VERTICAL, ++ y_align: Clutter.ActorAlign.CENTER, ++ 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', + () => this._setSelected(true)); +@@ -63,25 +168,26 @@ const AuthListItem = GObject.registerClass({ + this.remove_style_pseudo_class('selected'); + } + } +-}); ++} + +-var AuthList = GObject.registerClass({ +- Signals: { +- 'activate': { param_types: [GObject.TYPE_STRING] }, +- 'item-added': { param_types: [AuthListItem.$gtype] }, +- }, +-}, class AuthList extends St.BoxLayout { +- _init() { +- super._init({ +- vertical: true, ++export class AuthList extends St.BoxLayout { ++ static [GObject.signals] = { ++ 'activate': {param_types: [GObject.TYPE_STRING]}, ++ 'item-added': {param_types: [AuthListItem.$gtype]}, ++ }; ++ ++ static { ++ GObject.registerClass(this); ++ } ++ ++ constructor() { ++ super({ ++ orientation: Clutter.Orientation.VERTICAL, + style_class: 'login-dialog-auth-list-layout', +- x_align: Clutter.ActorAlign.START, + y_align: Clutter.ActorAlign.CENTER, ++ x_expand: true, + }); + +- this.label = new St.Label({ style_class: 'login-dialog-auth-list-title' }); +- this.add_child(this.label); +- + this._scrollView = new St.ScrollView({ + style_class: 'login-dialog-auth-list-view', + }); +@@ -135,11 +241,11 @@ var AuthList = GObject.registerClass({ + }); + } + +- addItem(key, text) { ++ addItem(key, content) { + this.removeItem(key); + +- let item = new AuthListItem(key, text); +- this._box.add(item); ++ const item = new AuthListItem(key, content); ++ this._box.add_child(item); + + this._items.set(key, item); + +@@ -169,8 +275,7 @@ var AuthList = GObject.registerClass({ + } + + clear() { +- this.label.text = ''; + this._box.destroy_all_children(); + this._items.clear(); + } +-}); ++} +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index 48f2c8cdd6..84c4575518 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -193,13 +193,32 @@ var AuthPrompt = GObject.registerClass({ + duration: MESSAGE_FADE_OUT_ANIMATION_TIME, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + onComplete: () => { ++ this._authListTitle.child.text = ''; + this._authList.clear(); + this._authList.hide(); + this._userVerifier.selectChoice(this._queryingService, key); + }, + }); + }); +- this._mainBox.add_child(this._authList); ++ this._inputWell.add_child(this._authList); ++ ++ // Use an insensitive button for the auth list title ++ // to get the same style as the auth list buttons ++ this._authListTitle = new St.Button({ ++ style_class: 'login-dialog-auth-list-title', ++ x_expand: true, ++ y_expand: true, ++ child: new St.Label({style_class: 'login-dialog-auth-list-title-label'}), ++ reactive: false, ++ can_focus: false, ++ }); ++ this._authList.bind_property('visible', ++ this._authListTitle, 'visible', ++ GObject.BindingFlags.SYNC_CREATE); ++ this._authList.bind_property('opacity', ++ this._authListTitle, 'opacity', ++ GObject.BindingFlags.SYNC_CREATE); ++ this._mainBox.add_child(this._authListTitle); + + this._entryArea = new St.Widget({ + style_class: 'login-dialog-prompt-entry-area', +@@ -560,6 +579,7 @@ var AuthPrompt = GObject.registerClass({ + + this._entryArea.hide(); + this.stopSpinning(); ++ this._authListTitle.child.text = ''; + this._authList.clear(); + this._authList.hide(); + +@@ -614,10 +634,10 @@ var AuthPrompt = GObject.registerClass({ + + setChoiceList(promptMessage, choiceList) { + this._authList.clear(); +- this._authList.label.text = promptMessage; +- for (let key in choiceList) { +- let text = choiceList[key]; +- this._authList.addItem(key, text); ++ this._authListTitle.child.text = promptMessage; ++ for (const key in choiceList) { ++ const content = choiceList[key]; ++ this._authList.addItem(key, content); + } + + this._entryArea.hide(); +diff --git a/js/gdm/util.js b/js/gdm/util.js +index 38a7015de8..b6327c5729 100644 +--- a/js/gdm/util.js ++++ b/js/gdm/util.js +@@ -762,7 +762,11 @@ var ShellUserVerifier = class { + if (!this.serviceIsForeground(serviceName)) + return; + +- this.emit('show-choice-list', serviceName, promptMessage, list.deep_unpack()); ++ 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) { +-- +2.51.0 + + +From f9b334f264d5d04b9bb460a28b245a72ddd3fc24 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Wed, 8 Oct 2025 18:48:07 +0200 +Subject: [PATCH 25/48] authPrompt: Let back button go back to step 1 instead + of full reset + +There can be some auth methods that would require multiple steps. In the +original behaviour when pressing back button, it would do a full reset +and display the user list. Now if we're in a multi-step flow (step > 1), +go back to step 1 instead of full reset. + +Show backButton in unlockDialog for these cases. +--- + js/gdm/authPrompt.js | 38 ++++++++++++++++++++++++++++---------- + 1 file changed, 28 insertions(+), 10 deletions(-) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index 84c4575518..ad7b890c7c 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -67,6 +67,7 @@ var AuthPrompt = GObject.registerClass({ + this._mode = mode; + this._defaultButtonWellActor = null; + this._cancelledRetries = 0; ++ this._promptStep = 0; + + this._idleMonitor = Meta.IdleMonitor.get_core(); + +@@ -98,8 +99,6 @@ var AuthPrompt = GObject.registerClass({ + }); + this.add_child(this._userWell); + +- this._hasCancelButton = this._mode === AuthPromptMode.UNLOCK_OR_LOG_IN; +- + this._initInputRow(); + + let capsLockPlaceholder = new St.Label(); +@@ -163,8 +162,8 @@ var AuthPrompt = GObject.registerClass({ + style_class: 'modal-dialog-button button cancel-button', + accessible_name: _('Cancel'), + button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE, +- reactive: this._hasCancelButton, +- can_focus: this._hasCancelButton, ++ reactive: true, ++ can_focus: true, + x_expand: true, + x_align: Clutter.ActorAlign.START, + y_align: Clutter.ActorAlign.CENTER, +@@ -176,10 +175,8 @@ var AuthPrompt = GObject.registerClass({ + pivot_point: new Graphene.Point({x: 1, y: 0}), + })); + +- if (this._hasCancelButton) +- this.cancelButton.connect('clicked', () => this.cancel()); +- else +- this.cancelButton.opacity = 0; ++ this.cancelButton.connect('clicked', () => this.cancel()); ++ this._updateCancelButton(); + this._mainBox.add_child(this.cancelButton); + + this._authList = new AuthList.AuthList(); +@@ -310,6 +307,16 @@ var AuthPrompt = GObject.registerClass({ + } catch (e) {} + } + ++ _updateCancelButton() { ++ if (this._mode === AuthPromptMode.UNLOCK_OR_LOG_IN) ++ return; ++ ++ const cancelVisible = this._promptStep > 1; ++ this.cancelButton.visible = cancelVisible; ++ this.cancelButton.reactive = cancelVisible; ++ this.cancelButton.can_focus = cancelVisible; ++ } ++ + showTimedLoginIndicator(time) { + let hold = new Batch.Hold(); + +@@ -396,6 +403,9 @@ var AuthPrompt = GObject.registerClass({ + this.clear(); + + this._queryingService = serviceName; ++ this._promptStep++; ++ this._updateCancelButton(); ++ + if (this._preemptiveAnswer) { + this._userVerifier.answerQuery(this._queryingService, this._preemptiveAnswer); + this._preemptiveAnswer = null; +@@ -420,6 +430,8 @@ var AuthPrompt = GObject.registerClass({ + this.clear(); + + this._queryingService = serviceName; ++ this._promptStep++; ++ this._updateCancelButton(); + + if (this._preemptiveAnswer) + this._preemptiveAnswer = null; +@@ -770,10 +782,10 @@ var AuthPrompt = GObject.registerClass({ + + const oldStatus = this.verificationStatus; + 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; ++ this._promptStep = 0; ++ this._updateCancelButton(); + + if (this._preemptiveAnswerWatchId) + this._idleMonitor.remove_watch(this._preemptiveAnswerWatchId); +@@ -861,6 +873,12 @@ var AuthPrompt = GObject.registerClass({ + if (this.verificationStatus == AuthPromptStatus.VERIFICATION_SUCCEEDED) + return; + ++ // If we're in a multi-step flow (step > 1), go back to step 1 instead of full reset ++ if (this._promptStep > 1) { ++ this.reset({softReset: true}); ++ return; ++ } ++ + if (this.verificationStatus === AuthPromptStatus.VERIFICATION_IN_PROGRESS) { + this._cancelledRetries++; + if (this._cancelledRetries > this._userVerifier.allowedFailures) +-- +2.51.0 + + +From 54e917b7974c0f1cdc12a5c00ff903912408b721 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Mon, 20 Oct 2025 17:24:40 +0200 +Subject: [PATCH 26/48] loginDialog: Vertically center authPrompt using fixed + 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 | 25 ++++++++++++++++++++++++- + 1 file changed, 24 insertions(+), 1 deletion(-) + +diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js +index 36a727f01f..8b0402ffdc 100644 +--- a/js/gdm/loginDialog.js ++++ b/js/gdm/loginDialog.js +@@ -34,6 +34,7 @@ const UserWidget = imports.ui.userWidget; + + const _FADE_ANIMATION_TIME = 250; + const _SCROLL_ANIMATION_TIME = 500; ++const _FIXED_TOP_ACTOR_HEIGHT = 400; + const _TIMED_LOGIN_IDLE_THRESHOLD = 5.0; + + var UserListItem = GObject.registerClass({ +@@ -582,6 +583,28 @@ var LoginDialog = GObject.registerClass({ + return actorBox; + } + ++ _getFixedTopActorAllocation(dialogBox, actor) { ++ const actorBox = new Clutter.ActorBox(); ++ ++ let [, , natWidth, natHeight] = actor.get_preferred_size(); ++ const dialogWidth = dialogBox.x2 - dialogBox.x1; ++ const dialogHeight = dialogBox.y2 - dialogBox.y1; ++ const centerX = dialogBox.x1 + dialogWidth / 2; ++ const centerY = dialogBox.y1 + dialogHeight / 2; ++ ++ 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); ++ actorBox.x2 = actorBox.x1 + natWidth; ++ actorBox.y2 = actorBox.y1 + natHeight; ++ ++ return actorBox; ++ } ++ + _getCenterActorAllocation(dialogBox, actor) { + let actorBox = new Clutter.ActorBox(); + +@@ -620,7 +643,7 @@ var LoginDialog = GObject.registerClass({ + let authPromptAllocation = null; + let authPromptWidth = 0; + if (this._authPrompt.visible) { +- authPromptAllocation = this._getCenterActorAllocation(dialogBox, this._authPrompt); ++ authPromptAllocation = this._getFixedTopActorAllocation(dialogBox, this._authPrompt); + authPromptWidth = authPromptAllocation.x2 - authPromptAllocation.x1; + } + +-- +2.51.0 + + +From 440192763a323db9e1b23d2d0e467d8c004f075f Mon Sep 17 00:00:00 2001 +From: Ray Strode +Date: Tue, 12 Nov 2024 14:26:30 -0500 +Subject: [PATCH 27/48] data: Add fingerprint and vcard icons + +Fingerprint icon will be used to inform when it's being run un the +background. + +Vcard icon will be used to inform on smartcard certificates list when they +have an Organization field in its subject. +--- + .../status/fingerprint-auth-symbolic.svg | 28 +++++++++++++++++++ + data/icons/scalable/status/vcard-symbolic.svg | 9 ++++++ + 2 files changed, 37 insertions(+) + create mode 100644 data/icons/scalable/status/fingerprint-auth-symbolic.svg + create mode 100644 data/icons/scalable/status/vcard-symbolic.svg + +diff --git a/data/icons/scalable/status/fingerprint-auth-symbolic.svg b/data/icons/scalable/status/fingerprint-auth-symbolic.svg +new file mode 100644 +index 0000000000..67cce39bc1 +--- /dev/null ++++ b/data/icons/scalable/status/fingerprint-auth-symbolic.svg +@@ -0,0 +1,28 @@ ++ ++ ++ ++image/svg+xmlauth-fingerprint +diff --git a/data/icons/scalable/status/vcard-symbolic.svg b/data/icons/scalable/status/vcard-symbolic.svg +new file mode 100644 +index 0000000000..1694f23645 +--- /dev/null ++++ b/data/icons/scalable/status/vcard-symbolic.svg +@@ -0,0 +1,9 @@ ++ ++ ++ ++ ++ ++ ++ ++ ++ +-- +2.51.0 + + +From 833e3979f1823836188043420f6cc44885c3c02f Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Mon, 18 Aug 2025 12:30:50 +0200 +Subject: [PATCH 28/48] gdm: Extract authentication service and role constants + to constants.js + +Create a dedicated const.js module to centralize GDM authentication-related +constants that are shared across multiple files. + +This prepares the codebase for upcoming commits that will use these +constants. +--- + js/gdm/authPrompt.js | 3 ++- + js/gdm/constants.js | 10 ++++++++++ + js/gdm/util.js | 32 +++++++++++++++----------------- + js/js-resources.gresource.xml | 1 + + 4 files changed, 28 insertions(+), 18 deletions(-) + create mode 100644 js/gdm/constants.js + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index ad7b890c7c..0aca776324 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -6,6 +6,7 @@ const { Clutter, Gio, GLib, GObject, Graphene, Meta, Pango, Shell, St } = import + const Animation = imports.ui.animation; + const AuthList = imports.gdm.authList; + const Batch = imports.gdm.batch; ++const Constants = imports.gdm.constants; + const GdmUtil = imports.gdm.util; + const OVirt = imports.gdm.oVirt; + const Vmware = imports.gdm.vmware; +@@ -456,7 +457,7 @@ var AuthPrompt = GObject.registerClass({ + // with a smartcard + // 2) Don't reset if we've already succeeded at verification and + // the user is getting logged in. +- if (this._userVerifier.serviceIsDefault(GdmUtil.SMARTCARD_SERVICE_NAME) && ++ if (this._userVerifier.serviceIsDefault(Constants.SMARTCARD_SERVICE_NAME) && + (this.verificationStatus === AuthPromptStatus.VERIFYING || + this.verificationStatus === AuthPromptStatus.VERIFICATION_IN_PROGRESS) && + this.smartcardDetected) +diff --git a/js/gdm/constants.js b/js/gdm/constants.js +new file mode 100644 +index 0000000000..2f37446c8a +--- /dev/null ++++ b/js/gdm/constants.js +@@ -0,0 +1,10 @@ ++// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- ++ ++export const PASSWORD_ROLE_NAME = 'password'; ++export const SMARTCARD_ROLE_NAME = 'smartcard'; ++export const FINGERPRINT_ROLE_NAME = 'fingerprint'; ++ ++export const PASSWORD_SERVICE_NAME = 'gdm-password'; ++export const SMARTCARD_SERVICE_NAME = 'gdm-smartcard'; ++export const FINGERPRINT_SERVICE_NAME = 'gdm-fingerprint'; ++export const SWITCHABLE_AUTH_SERVICE_NAME = 'gdm-switchable-auth'; +diff --git a/js/gdm/util.js b/js/gdm/util.js +index b6327c5729..4a8bf09d18 100644 +--- a/js/gdm/util.js ++++ b/js/gdm/util.js +@@ -6,6 +6,7 @@ const { Clutter, Gdm, Gio, GLib } = imports.gi; + const Signals = imports.signals; + + const Batch = imports.gdm.batch; ++const Constants = imports.gdm.constants; + const OVirt = imports.gdm.oVirt; + const Vmware = imports.gdm.vmware; + const Main = imports.ui.main; +@@ -29,9 +30,6 @@ Gio._promisify(Gdm.UserVerifierProxy.prototype, + Gio._promisify(Gio.DBusProxy.prototype, + 'call', 'call_finish'); + +-var PASSWORD_SERVICE_NAME = 'gdm-password'; +-var FINGERPRINT_SERVICE_NAME = 'gdm-fingerprint'; +-var SMARTCARD_SERVICE_NAME = 'gdm-smartcard'; + var FADE_ANIMATION_TIME = 160; + var CLONE_FADE_ANIMATION_TIME = 250; + +@@ -476,7 +474,7 @@ var ShellUserVerifier = class { + this._updateDefaultService(); + + if (this._userVerifier && +- !this._activeServices.has(FINGERPRINT_SERVICE_NAME)) { ++ !this._activeServices.has(Constants.FINGERPRINT_SERVICE_NAME)) { + if (!this._hold?.isAcquired()) + this._hold = new Batch.Hold(); + await this._maybeStartFingerprintVerification(); +@@ -529,8 +527,8 @@ var ShellUserVerifier = class { + this.smartcardDetected = smartcardDetected; + + if (this.smartcardDetected) +- this._preemptingService = SMARTCARD_SERVICE_NAME; +- else if (this._preemptingService == SMARTCARD_SERVICE_NAME) ++ this._preemptingService = Constants.SMARTCARD_SERVICE_NAME; ++ else if (this._preemptingService === Constants.SMARTCARD_SERVICE_NAME) + this._preemptingService = null; + + this._updateDefaultService(); +@@ -654,7 +652,7 @@ var ShellUserVerifier = class { + + serviceIsFingerprint(serviceName) { + return this._fingerprintReaderType !== FingerprintReaderType.NONE && +- serviceName === FINGERPRINT_SERVICE_NAME; ++ serviceName === Constants.FINGERPRINT_SERVICE_NAME; + } + + _onSettingsChanged() { +@@ -671,7 +669,7 @@ var ShellUserVerifier = class { + this._fingerprintManager = null; + this._fingerprintReaderType = FingerprintReaderType.NONE; + +- if (this._activeServices.has(FINGERPRINT_SERVICE_NAME)) ++ if (this._activeServices.has(Constants.FINGERPRINT_SERVICE_NAME)) + needsReset = true; + } + +@@ -682,7 +680,7 @@ var ShellUserVerifier = class { + this._smartcardManager.disconnect(this._smartcardRemovedId); + this._smartcardManager = null; + +- if (this._activeServices.has(SMARTCARD_SERVICE_NAME)) ++ if (this._activeServices.has(Constants.SMARTCARD_SERVICE_NAME)) + needsReset = true; + } + +@@ -692,13 +690,13 @@ var ShellUserVerifier = class { + + _getDetectedDefaultService() { + if (this._smartcardManager?.loggedInWithToken()) +- return SMARTCARD_SERVICE_NAME; ++ return Constants.SMARTCARD_SERVICE_NAME; + else if (this._settings.get_boolean(PASSWORD_AUTHENTICATION_KEY)) +- return PASSWORD_SERVICE_NAME; ++ return Constants.PASSWORD_SERVICE_NAME; + else if (this._smartcardManager) +- return SMARTCARD_SERVICE_NAME; ++ return Constants.SMARTCARD_SERVICE_NAME; + else if (this._fingerprintReaderType !== FingerprintReaderType.NONE) +- return FINGERPRINT_SERVICE_NAME; ++ return Constants.FINGERPRINT_SERVICE_NAME; + return null; + } + +@@ -708,7 +706,7 @@ var ShellUserVerifier = class { + + if (!this._defaultService) { + log("no authentication service is enabled, using password authentication"); +- this._defaultService = PASSWORD_SERVICE_NAME; ++ this._defaultService = Constants.PASSWORD_SERVICE_NAME; + } + + if (oldDefaultService && +@@ -754,8 +752,8 @@ var ShellUserVerifier = class { + async _maybeStartFingerprintVerification() { + if (this._userName && + this._fingerprintReaderType !== FingerprintReaderType.NONE && +- !this.serviceIsForeground(FINGERPRINT_SERVICE_NAME)) +- await this._startService(FINGERPRINT_SERVICE_NAME); ++ !this.serviceIsForeground(Constants.FINGERPRINT_SERVICE_NAME)) ++ await this._startService(Constants.FINGERPRINT_SERVICE_NAME); + } + + _onChoiceListQuery(client, serviceName, promptMessage, list) { +@@ -880,7 +878,7 @@ var ShellUserVerifier = class { + } + + _verificationFailed(serviceName, shouldRetry) { +- if (serviceName === FINGERPRINT_SERVICE_NAME) { ++ if (serviceName === Constants.FINGERPRINT_SERVICE_NAME) { + if (this._fingerprintFailedId) + GLib.source_remove(this._fingerprintFailedId); + } +diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml +index b2c603a55a..ec6dee70b2 100644 +--- a/js/js-resources.gresource.xml ++++ b/js/js-resources.gresource.xml +@@ -4,6 +4,7 @@ + gdm/authList.js + gdm/authPrompt.js + gdm/batch.js ++ gdm/constants.js + gdm/loginDialog.js + gdm/oVirt.js + gdm/credentialManager.js +-- +2.51.0 + + +From 7763c1b3b91810be91ce2be4d8df05b2aabb38e4 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Wed, 11 Mar 2026 11:41:40 +0100 +Subject: [PATCH 29/48] gdm/util: Add helper functions to get future mechanisms + metadata + +1. isSelectable() to detect when a mechanism is selectable or runs in + background. +2. getNonSelectableIconName() to get the icon name for a non selectable mechanism + +Next commits will use these functions. +--- + js/gdm/util.js | 34 ++++++++++++++++++++++++++++++++++ + 1 file changed, 34 insertions(+) + +diff --git a/js/gdm/util.js b/js/gdm/util.js +index 4a8bf09d18..691442ad90 100644 +--- a/js/gdm/util.js ++++ b/js/gdm/util.js +@@ -137,6 +137,40 @@ function cloneAndFadeOutActor(actor) { + return hold; + } + ++/** ++ * @param {object} mechanism ++ * @returns {boolean} ++ */ ++export function isSelectable(mechanism) { ++ switch (mechanism.role) { ++ case Constants.PASSWORD_ROLE_NAME: ++ case Constants.SMARTCARD_ROLE_NAME: ++ return true; ++ case Constants.FINGERPRINT_ROLE_NAME: ++ return false; ++ default: ++ throw new Error(`Failed checking mechanism is selectable: ${mechanism.role}`); ++ } ++} ++ ++/** ++ * @param {object} mechanism ++ * @returns {string} ++ */ ++export function getNonSelectableIconName(mechanism) { ++ // This is only used for non selectable mechanisms. ++ // Currently only fingerprint is non selectable ++ if (isSelectable(mechanism)) ++ throw new Error(`Failed getting mechanism icon: ${mechanism.role}, is selectable`); ++ ++ switch (mechanism.role) { ++ case Constants.FINGERPRINT_ROLE_NAME: ++ return 'fingerprint-auth-symbolic'; ++ default: ++ throw new Error(`Failed getting mechanism icon: ${mechanism.role}`); ++ } ++} ++ + var ShellUserVerifier = class { + constructor(client, params) { + params = Params.parse(params, { reauthenticationOnly: false }); +-- +2.51.0 + + +From a8303da3be45bec905d11e7e8a64ffa9088f6887 Mon Sep 17 00:00:00 2001 +From: Ray Strode +Date: Tue, 6 Feb 2024 11:04:20 -0500 +Subject: [PATCH 30/48] gdm: Add new AuthMenuButton control + +The latest login screen designs show a new "Login Options" menu in the +corner for session selection and login methods. + +As a first step toward acheiving that goal, this commit adds a new +AuthMenuButton class. It's a fairly generic control derived from +the code used to show the sessions menu button. One key difference +is it allows a multi-valued, partial key for its entries. This provides +flexibility that we'll need to leverage later. + +Also add AuthMenuButtonIndicator which derives from AuthMenuButton to +display icons to inform about non selectable background authentication +methods in use, it's non-interactive. + +Nothing uses these new classes yet. A subsequent commit will change the +sessions menu button code over to use it, and a commit after that will +use it for Login Options. +--- + js/gdm/authMenuButton.js | 532 ++++++++++++++++++++++++++++++++++ + js/js-resources.gresource.xml | 1 + + 2 files changed, 533 insertions(+) + create mode 100644 js/gdm/authMenuButton.js + +diff --git a/js/gdm/authMenuButton.js b/js/gdm/authMenuButton.js +new file mode 100644 +index 0000000000..9b7cebf6d8 +--- /dev/null ++++ b/js/gdm/authMenuButton.js +@@ -0,0 +1,532 @@ ++// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- ++/* ++ * Copyright 2024 Red Hat, Inc ++ * ++ * This program is free software; you can redistribute it and/or modify ++ * it under the terms of the GNU General Public License as published by ++ * the Free Software Foundation; either version 2, or (at your option) ++ * any later version. ++ * ++ * This program is distributed in the hope that it will be useful, ++ * but WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++ * GNU General Public License for more details. ++ * ++ * You should have received a copy of the GNU General Public License ++ * along with this program; if not, see . ++ */ ++ ++/* ++ * AuthMenuButton implements a menu button that is used to manage login options ++ * (authentication methods, session) ++ * ++ * Item objects are mapped to menu items internally using the item objects ++ * themselves as their own key. ++ * Each item can have arbitrary properties, but certain properties have special ++ * meaning: ++ * - name: (required) Display text for the item ++ * - iconName: Icon to show for the item and optionally in the button ++ * - description: Additional descriptive text shown below the name ++ * - sectionName: Groups items into labeled sections ++ * ++ * The button supports searching for items using partial criteria objects. Any ++ * item matching all non-null properties in the criteria will be returned. ++ * For example: ++ * {sectionName: 'foo'} matches all items in section 'foo' ++ * {name: 'bar', sectionName: 'foo'} matches the item named 'bar' in ++ * section 'foo' ++ * ++ * Items within sections can be ordered using the sectionOrder parameter during ++ * initialization. Sections not in sectionOrder appear at the end. ++ * ++ * Only one item per section can be active at a time. Setting a new active item ++ * deactivates any other active item in the same section. ++ */ ++ ++import Atk from 'gi://Atk'; ++import Clutter from 'gi://Clutter'; ++import GObject from 'gi://GObject'; ++import Shell from 'gi://Shell'; ++import St from 'gi://St'; ++ ++import * as BoxPointer from '../ui/boxpointer.js'; ++import * as Main from '../ui/main.js'; ++import * as PopupMenu from '../ui/popupMenu.js'; ++ ++const VISIBILITY_ANIMATION_TIME = 200; ++ ++class AuthMenuItemSection extends PopupMenu.PopupMenuSection { ++ constructor(sectionName) { ++ super(); ++ ++ this.actor.add_style_class_name('login-dialog-auth-menu-item-section'); ++ ++ if (sectionName) { ++ const itemSection = new PopupMenu.PopupMenuItem(sectionName, { ++ reactive: false, ++ can_focus: false, ++ }); ++ itemSection.label.style_class = 'login-dialog-auth-menu-item-section-label'; ++ itemSection.label.x_expand = true; ++ itemSection.label.x_align = Clutter.ActorAlign.CENTER; ++ this.addMenuItem(itemSection); ++ } ++ } ++} ++ ++class AuthMenuItem extends PopupMenu.PopupBaseMenuItem { ++ static { ++ GObject.registerClass(this); ++ } ++ ++ constructor(item, params = {}) { ++ super(params); ++ ++ if (item.description && item.iconName) { ++ const icon = new St.Icon({ ++ style_class: 'login-dialog-auth-menu-item-icon', ++ icon_name: item.iconName, ++ x_align: Clutter.ActorAlign.CENTER, ++ }); ++ this.add_child(icon); ++ ++ const box = new St.BoxLayout({ ++ vertical: true, ++ style_class: 'login-dialog-auth-menu-item-box', ++ }); ++ const nameLabel = new St.Label({ ++ text: item.name, ++ style_class: 'login-dialog-auth-menu-item-box-name', ++ }); ++ const descriptionLabel = new St.Label({ ++ text: item.description, ++ style_class: 'login-dialog-auth-menu-item-box-description', ++ }); ++ box.add_child(nameLabel); ++ box.add_child(descriptionLabel); ++ this.add_child(box); ++ ++ this.label_actor = nameLabel; ++ } else { ++ // When no extra info is provided, use the default style ++ const nameLabel = new St.Label({ ++ text: item.name, ++ y_expand: true, ++ y_align: Clutter.ActorAlign.CENTER, ++ }); ++ this.add_child(nameLabel); ++ this.label_actor = nameLabel; ++ } ++ ++ this.setOrnament(PopupMenu.Ornament.HIDDEN); ++ } ++} ++ ++export class AuthMenuButton extends St.Button { ++ static [GObject.properties] = { ++ 'title': GObject.ParamSpec.string( ++ 'title', null, null, ++ GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY, ++ ''), ++ 'icon-name': GObject.ParamSpec.string( ++ 'icon-name', null, null, ++ GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY, ++ ''), ++ 'read-only': GObject.ParamSpec.boolean( ++ 'read-only', null, null, ++ GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY, ++ 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, ++ style_class: 'login-dialog-button login-dialog-auth-menu-button', ++ reactive: true, ++ track_hover: true, ++ can_focus: true, ++ accessible_name: params.title, ++ accessible_role: Atk.Role.MENU, ++ x_align: Clutter.ActorAlign.CENTER, ++ y_align: Clutter.ActorAlign.CENTER, ++ }); ++ this.child = this._createChild(); ++ ++ this._menu = new PopupMenu.PopupMenu(this, 0, St.Side.BOTTOM); ++ this._menu.box.add_style_class_name('login-dialog-auth-menu-button-popup'); ++ Main.uiGroup.add_child(this._menu.actor); ++ this._menu.actor.hide(); ++ ++ this._manager = new PopupMenu.PopupMenuManager(this, ++ {actionMode: Shell.ActionMode.NONE}); ++ this._manager.addMenu(this._menu); ++ ++ this.connect('clicked', () => { ++ if (this.readOnly && this._getVisibleItemsCount() === 1) ++ return; ++ this._menu.toggle(); ++ }); ++ ++ this._items = new Map(); ++ this._sections = new Map(); ++ this._activeItems = new Set(); ++ this.updateSensitivity(true); ++ } ++ ++ _createChild() { ++ return new St.Icon({icon_name: this.iconName}); ++ } ++ ++ _getMenuItem(item) { ++ if (!item) ++ return null; ++ ++ return this._items.get(JSON.stringify(item)); ++ } ++ ++ _getVisibleItemsCount() { ++ return Array.from(this._items.values()).filter(item => item.visible).length; ++ } ++ ++ updateReactive(reactive) { ++ this.reactive = reactive; ++ this.can_focus = reactive; ++ } ++ ++ updateSensitivity(sensitive) { ++ this._sensitive = sensitive; ++ ++ const visibleItems = this._getVisibleItemsCount(); ++ if (visibleItems === 0 || (visibleItems <= 1 && !this.readOnly)) ++ sensitive = false; ++ ++ this.reactive = sensitive; ++ this.can_focus = sensitive; ++ this._menu.close(BoxPointer.PopupAnimation.NONE); ++ ++ if (this.animateVisibility) { ++ if (sensitive) { ++ this.opacity = 0; ++ this.visible = true; ++ this.ease({ ++ opacity: 255, ++ duration: VISIBILITY_ANIMATION_TIME, ++ mode: Clutter.AnimationMode.EASE_OUT_QUAD, ++ }); ++ } else { ++ this.ease({ ++ opacity: 0, ++ duration: VISIBILITY_ANIMATION_TIME, ++ mode: Clutter.AnimationMode.EASE_OUT_QUAD, ++ onComplete: () => { ++ this.visible = false; ++ }, ++ }); ++ } ++ } else { ++ this.opacity = sensitive ? 255 : 0; ++ this.visible = sensitive; ++ } ++ } ++ ++ _updateOrnament() { ++ for (const menuItem of this._items.values()) ++ menuItem.setOrnament(PopupMenu.Ornament.NO_DOT); ++ ++ for (const itemKey of this._activeItems) { ++ const menuItem = this._getMenuItem(JSON.parse(itemKey)); ++ if (menuItem) ++ menuItem.setOrnament(PopupMenu.Ornament.DOT); ++ } ++ } ++ ++ _deepEquals(a, b) { ++ if (a === b) ++ return true; ++ ++ if (a == null || b == null) ++ return false; ++ ++ const keysA = Object.keys(a); ++ const keysB = Object.keys(b); ++ ++ if (keysA.length !== keysB.length) ++ return false; ++ ++ for (const key of keysA) { ++ if (!keysB.includes(key)) ++ return false; ++ ++ if (!this._deepEquals(a[key], b[key])) ++ return false; ++ } ++ ++ return true; ++ } ++ ++ _findItems(searchCriteria) { ++ const items = []; ++ for (const itemKey of this._items.keys()) { ++ const item = JSON.parse(itemKey); ++ ++ let criteriaMismatch = false; ++ for (const key of Object.keys(searchCriteria)) { ++ if (!searchCriteria[key]) ++ continue; ++ ++ if (this._deepEquals(item[key], searchCriteria[key])) ++ continue; ++ ++ criteriaMismatch = true; ++ break; ++ } ++ ++ if (criteriaMismatch) ++ continue; ++ ++ items.push(itemKey); ++ } ++ ++ return items; ++ } ++ ++ getItems(searchCriteria = {}) { ++ return this._findItems(searchCriteria).map(itemKey => JSON.parse(itemKey)); ++ } ++ ++ clearItems(searchCriteria = {}) { ++ this._findItems(searchCriteria).forEach(itemKey => { ++ const menuItem = this._items.get(itemKey); ++ this._activeItems.delete(itemKey); ++ menuItem.destroy(); ++ this._items.delete(itemKey); ++ }); ++ ++ this._sections.keys().forEach(sectionName => { ++ const itemsInSection = this._findItems({sectionName}); ++ if (itemsInSection.length === 0) { ++ const section = this._sections.get(sectionName); ++ if (section) { ++ section.destroy(); ++ this._sections.delete(sectionName); ++ } ++ } ++ }); ++ ++ this._updateVisibility(); ++ this.updateSensitivity(this._sensitive); ++ } ++ ++ addItem(item) { ++ const itemKey = JSON.stringify(item); ++ if (this._items.has(itemKey)) ++ throw new Error(`Duplicate item ${itemKey}`); ++ ++ if (!item.name) ++ throw new Error(`item ${itemKey} lacks name`); ++ ++ const menuItem = new AuthMenuItem(item, {reactive: !this.readOnly}); ++ menuItem.connect('activate', () => { ++ this.setActiveItem(item); ++ }); ++ ++ const section = this._getSection(item.sectionName); ++ section.addMenuItem(menuItem); ++ ++ this._items.set(itemKey, menuItem); ++ this._updateVisibility(); ++ this.updateSensitivity(this._sensitive); ++ } ++ ++ _getSection(sectionName) { ++ const key = sectionName ?? null; ++ let section = this._sections.get(key); ++ if (section) ++ return section; ++ ++ section = new AuthMenuItemSection(sectionName); ++ ++ if (key === null) { ++ // Empty section is always last ++ this._menu.addMenuItem(section); ++ } else { ++ const orderIndex = this.sectionOrder.indexOf(sectionName); ++ if (orderIndex !== -1) { ++ // Section is in sectionOrder, add at its position ++ this._menu.addMenuItem(section, orderIndex); ++ } else { ++ // Section not in sectionOrder, add before empty section if it exists ++ const emptySection = this._sections.get(null); ++ if (emptySection) { ++ const emptyIndex = this._menu._getMenuItems().indexOf(emptySection); ++ this._menu.addMenuItem(section, emptyIndex); ++ } else { ++ this._menu.addMenuItem(section); ++ } ++ } ++ } ++ ++ this._sections.set(key, section); ++ ++ return section; ++ } ++ ++ _updateVisibility() { ++ const visibleSections = this._getVisibleSections(); ++ ++ for (const [sectionName, section] of this._sections) ++ section.actor.visible = visibleSections.includes(sectionName); ++ ++ this.visible = visibleSections.length > 0; ++ } ++ ++ _getVisibleSections() { ++ return Array.from(this._sections.keys()).filter(sectionName => ++ this._getSectionItemCount(sectionName) > 1 ++ ); ++ } ++ ++ _getSectionItemCount(sectionName) { ++ return this._findItems({sectionName}).length; ++ } ++ ++ _resolveItem(searchCriteria) { ++ const itemKeys = this._findItems(searchCriteria); ++ ++ if (!itemKeys.length) ++ throw new Error(`Unknown item ${JSON.stringify(searchCriteria)}`); ++ ++ if (itemKeys.length > 1) ++ throw new Error(`Matched multiple items with criteria ${JSON.stringify(searchCriteria)}`); ++ ++ const item = JSON.parse(itemKeys[0]); ++ const menuItem = this._items.get(itemKeys[0]); ++ return {item, menuItem}; ++ } ++ ++ setActiveItem(searchCriteria) { ++ const {item} = this._resolveItem(searchCriteria); ++ const itemKey = JSON.stringify(item); ++ ++ if (this._activeItems.has(itemKey)) ++ return; ++ ++ const sectionName = item.sectionName ?? null; ++ const activeInSection = this._findItems({sectionName}) ++ .filter(key => this._activeItems.has(key)); ++ ++ for (const key of activeInSection) ++ this._activeItems.delete(key); ++ ++ this._activeItems.add(itemKey); ++ this._updateOrnament(); ++ this.emit('active-item-changed', sectionName); ++ } ++ ++ getActiveItem(searchCriteria = {}) { ++ const activeKeys = this._findItems(searchCriteria) ++ .filter(key => this._activeItems.has(key)); ++ ++ if (activeKeys.length === 0) ++ return null; ++ ++ if (activeKeys.length > 1) ++ throw new Error(`Multiple active items found with criteria ${JSON.stringify(searchCriteria)}`); ++ ++ return JSON.parse(activeKeys[0]); ++ } ++ ++ closeMenu() { ++ this._menu.close(); ++ } ++} ++ ++export class AuthMenuButtonIndicator extends AuthMenuButton { ++ static { ++ GObject.registerClass(this); ++ } ++ ++ constructor(params = {}) { ++ params.readOnly ??= true; ++ super(params); ++ ++ this.add_style_class_name('login-dialog-auth-menu-button-indicator'); ++ } ++ ++ _createChild() { ++ const container = new St.BoxLayout({ ++ x_align: Clutter.ActorAlign.START, ++ }); ++ ++ this._iconsBox = new St.BoxLayout({ ++ style_class: 'login-dialog-auth-menu-button-indicator-icons', ++ x_expand: true, ++ x_align: Clutter.ActorAlign.CENTER, ++ }); ++ container.add_child(this._iconsBox); ++ ++ this._descriptionLabel = new St.Label({ ++ style_class: 'login-dialog-auth-menu-button-indicator-description', ++ y_align: Clutter.ActorAlign.CENTER, ++ }); ++ this._descriptionLabel.bind_property_full('text', ++ this._descriptionLabel, 'visible', ++ GObject.BindingFlags.SYNC_CREATE, ++ (bind, source) => [true, !!source], ++ null); ++ container.add_child(this._descriptionLabel); ++ ++ return container; ++ } ++ ++ addItem(item) { ++ if (item.iconName) { ++ const icon = new St.Icon({ ++ icon_name: item.iconName, ++ style_class: 'login-dialog-auth-menu-button-indicator-icon', ++ }); ++ this._iconsBox.add_child(icon); ++ } ++ ++ super.addItem(item); ++ } ++ ++ clearItems(searchCriteria = {}) { ++ this._findItems(searchCriteria).forEach(itemKey => { ++ const item = JSON.parse(itemKey); ++ if (item.iconName) { ++ this._iconsBox.get_children() ++ .find(icon => icon.icon_name === item.iconName) ++ ?.destroy(); ++ } ++ }); ++ ++ super.clearItems(searchCriteria); ++ this.updateDescriptionLabel(); ++ } ++ ++ updateDescriptionLabel() { ++ 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 ec6dee70b2..105076b40e 100644 +--- a/js/js-resources.gresource.xml ++++ b/js/js-resources.gresource.xml +@@ -2,6 +2,7 @@ + + + gdm/authList.js ++ gdm/authMenuButton.js + gdm/authPrompt.js + gdm/batch.js + gdm/constants.js +-- +2.51.0 + + +From b016701668a727633c2a0684d72d12861da03597 Mon Sep 17 00:00:00 2001 +From: Ray Strode +Date: Tue, 6 Feb 2024 11:11:32 -0500 +Subject: [PATCH 31/48] loginDialog: Port sessions menu over to AuthMenuButton + +Now that AuthMenuButton exists, we should use it. + +This commit changes the session menu over to use the new +control. +--- + js/gdm/loginDialog.js | 178 +++++++++++++++--------------------------- + 1 file changed, 64 insertions(+), 114 deletions(-) + +diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js +index 8b0402ffdc..c85706f1d4 100644 +--- a/js/gdm/loginDialog.js ++++ b/js/gdm/loginDialog.js +@@ -20,9 +20,9 @@ + const { AccountsService, Atk, Clutter, Gdm, Gio, + GLib, GObject, Meta, Pango, Shell, St } = imports.gi; + ++const AuthMenuButton = imports.gdm.authMenuButton; + const AuthPrompt = imports.gdm.authPrompt; + const Batch = imports.gdm.batch; +-const BoxPointer = imports.ui.boxpointer; + const CtrlAltTab = imports.ui.ctrlAltTab; + const GdmUtil = imports.gdm.util; + const Layout = imports.ui.layout; +@@ -36,6 +36,7 @@ const _FADE_ANIMATION_TIME = 250; + const _SCROLL_ANIMATION_TIME = 500; + const _FIXED_TOP_ACTOR_HEIGHT = 400; + const _TIMED_LOGIN_IDLE_THRESHOLD = 5.0; ++const _SESSION_TYPE_SECTION_NAME = _('Session Type'); + + var UserListItem = GObject.registerClass({ + Signals: { 'activate': {} }, +@@ -305,102 +306,6 @@ var UserList = GObject.registerClass({ + } + }); + +-var SessionMenuButton = GObject.registerClass({ +- Signals: { 'session-activated': { param_types: [GObject.TYPE_STRING] } }, +-}, class SessionMenuButton extends St.Bin { +- _init() { +- let gearIcon = new St.Icon({ icon_name: 'emblem-system-symbolic' }); +- let button = new St.Button({ +- style_class: 'modal-dialog-button button login-dialog-session-list-button', +- reactive: true, +- track_hover: true, +- can_focus: true, +- accessible_name: _("Choose Session"), +- accessible_role: Atk.Role.MENU, +- x_align: Clutter.ActorAlign.CENTER, +- y_align: Clutter.ActorAlign.CENTER, +- child: gearIcon, +- }); +- +- super._init({ child: button }); +- this._button = button; +- +- this._menu = new PopupMenu.PopupMenu(this._button, 0, St.Side.BOTTOM); +- Main.uiGroup.add_actor(this._menu.actor); +- this._menu.actor.hide(); +- +- this._menu.connect('open-state-changed', (menu, isOpen) => { +- if (isOpen) +- this._button.add_style_pseudo_class('active'); +- else +- this._button.remove_style_pseudo_class('active'); +- }); +- +- this._manager = new PopupMenu.PopupMenuManager(this._button, +- { actionMode: Shell.ActionMode.NONE }); +- this._manager.addMenu(this._menu); +- +- this._button.connect('clicked', () => this._menu.toggle()); +- +- this._items = {}; +- this._activeSessionId = null; +- this._populate(); +- } +- +- updateSensitivity(sensitive) { +- this._button.reactive = sensitive; +- this._button.can_focus = sensitive; +- this.opacity = sensitive ? 255 : 0; +- this._menu.close(BoxPointer.PopupAnimation.NONE); +- } +- +- _updateOrnament() { +- let itemIds = Object.keys(this._items); +- for (let i = 0; i < itemIds.length; i++) { +- if (itemIds[i] == this._activeSessionId) +- this._items[itemIds[i]].setOrnament(PopupMenu.Ornament.DOT); +- else +- this._items[itemIds[i]].setOrnament(PopupMenu.Ornament.NONE); +- } +- } +- +- setActiveSession(sessionId) { +- if (sessionId == this._activeSessionId) +- return; +- +- this._activeSessionId = sessionId; +- this._updateOrnament(); +- } +- +- close() { +- this._menu.close(); +- } +- +- _populate() { +- let ids = Gdm.get_session_ids(); +- ids.sort(); +- +- if (ids.length <= 1) { +- this._button.hide(); +- return; +- } +- +- for (let i = 0; i < ids.length; i++) { +- let [sessionName, sessionDescription_] = Gdm.get_session_name_and_description(ids[i]); +- +- let id = ids[i]; +- let item = new PopupMenu.PopupMenuItem(sessionName); +- this._menu.addMenuItem(item); +- this._items[id] = item; +- +- item.connect('activate', () => { +- this.setActiveSession(id); +- this.emit('session-activated', this._activeSessionId); +- }); +- } +- } +-}); +- + var LoginDialog = GObject.registerClass({ + Signals: { + 'failed': {}, +@@ -453,6 +358,7 @@ var LoginDialog = GObject.registerClass({ + this._authPrompt.connect('prompted', this._onPrompted.bind(this)); + this._authPrompt.connect('reset', this._onReset.bind(this)); + this._authPrompt.connect('verification-complete', this._onVerificationComplete.bind(this)); ++ this._authPrompt.connect('loading', this._onLoading.bind(this)); + this._authPrompt.hide(); + this.add_child(this._authPrompt); + +@@ -500,14 +406,7 @@ var LoginDialog = GObject.registerClass({ + }); + this.add_child(this._bottomButtonGroup); + +- this._sessionMenuButton = new SessionMenuButton(); +- this._sessionMenuButton.connect('session-activated', +- (list, sessionId) => { +- this._greeter.call_select_session_sync(sessionId, null); +- }); +- this._sessionMenuButton.opacity = 0; +- this._sessionMenuButton.show(); +- this._bottomButtonGroup.add_child(this._sessionMenuButton); ++ this._createAuthMenuButton(); + + this._logoBin = new St.Widget({ style_class: 'login-dialog-logo-bin', + x_align: Clutter.ActorAlign.CENTER, +@@ -538,6 +437,54 @@ var LoginDialog = GObject.registerClass({ + this._updateDisableUserList.bind(this)); + } + ++ _createAuthMenuButton() { ++ this._authMenuButton = new AuthMenuButton.AuthMenuButton({ ++ title: _('Login Options'), ++ iconName: 'cog-wheel-symbolic', ++ sectionOrder: [_SESSION_TYPE_SECTION_NAME], ++ }); ++ this._authMenuButton.updateSensitivity(false); ++ ++ this._updateSessions(); ++ ++ this._authMenuButton.connect('active-item-changed', (_button, sectionName) => { ++ const item = this._authMenuButton.getActiveItem({sectionName}); ++ if (!item) ++ return; ++ ++ if (sectionName === _SESSION_TYPE_SECTION_NAME) ++ this._greeter.call_select_session_sync(item.id, null); ++ ++ this._authMenuButton.closeMenu(); ++ }); ++ this._bottomButtonGroup.add_child(this._authMenuButton); ++ } ++ ++ _updateSessions() { ++ this._authMenuButton.clearItems({ ++ sectionName: _SESSION_TYPE_SECTION_NAME, ++ }); ++ ++ const ids = Gdm.get_session_ids(); ++ if (ids.length <= 1) ++ return; ++ ++ const sessions = ids.map(id => { ++ const [sessionName] = Gdm.get_session_name_and_description(id); ++ return {id, sessionName}; ++ }); ++ ++ sessions.sort((a, b) => a.sessionName.localeCompare(b.sessionName)); ++ ++ for (const {id, sessionName} of sessions) { ++ this._authMenuButton.addItem({ ++ sectionName: _SESSION_TYPE_SECTION_NAME, ++ name: sessionName, ++ id, ++ }); ++ } ++ } ++ + _getBannerAllocation(dialogBox) { + let actorBox = new Clutter.ActorBox(); + +@@ -865,10 +812,8 @@ var LoginDialog = GObject.registerClass({ + } + + _onPrompted() { +- const showSessionMenu = this._shouldShowSessionMenuButton(); ++ this._authMenuButton.updateSensitivity(this._shouldShowAuthMenu()); + +- this._sessionMenuButton.updateSensitivity(showSessionMenu); +- this._sessionMenuButton.visible = showSessionMenu; + this._showPrompt(); + } + +@@ -890,7 +835,6 @@ var LoginDialog = GObject.registerClass({ + + _onReset(authPrompt, resetType) { + this._resetGreeterProxy(); +- this._sessionMenuButton.updateSensitivity(true); + + const previousUser = this._user; + this._user = null; +@@ -924,11 +868,18 @@ var LoginDialog = GObject.registerClass({ + }); + } + ++ _onLoading(_authPrompt, isLoading) { ++ this._authMenuButton.updateReactive(!isLoading); ++ } ++ + _onDefaultSessionChanged(client, sessionId) { +- this._sessionMenuButton.setActiveSession(sessionId); ++ this._authMenuButton.setActiveItem({ ++ sectionName: _SESSION_TYPE_SECTION_NAME, ++ id: sessionId, ++ }); + } + +- _shouldShowSessionMenuButton() { ++ _shouldShowAuthMenu() { + const visibleStatuses = [ + AuthPrompt.AuthPromptStatus.VERIFYING, + AuthPrompt.AuthPromptStatus.VERIFICATION_FAILED, +@@ -990,7 +941,7 @@ var LoginDialog = GObject.registerClass({ + }); + this._updateCancelButton(); + +- this._sessionMenuButton.updateSensitivity(false); ++ this._authMenuButton.updateSensitivity(false); + this._authPrompt.updateSensitivity({sensitive: true}); + this._showPrompt(); + } +@@ -1213,8 +1164,7 @@ var LoginDialog = GObject.registerClass({ + this._ensureUserListLoaded(); + this._authPrompt.hide(); + this._hideBannerView(); +- this._sessionMenuButton.close(); +- this._sessionMenuButton.hide(); ++ this._authMenuButton.updateSensitivity(false); + this._setUserListExpanded(true); + this._notListedButton.show(); + this._userList.grab_key_focus(); +-- +2.51.0 + + +From 22aca4284e6e61d2e7ddede78a96484a3bab1225 Mon Sep 17 00:00:00 2001 +From: Ray Strode +Date: Tue, 6 Feb 2024 13:40:26 -0500 +Subject: [PATCH 32/48] loginDialog: Add login options menu to AuthMenuButton +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Now the authMenuButton allows selecting a session type and a login +mechanism. + +authPrompt is in charge of: + * Informing of the available login mechanisms via the "mechanisms-changed" + signal, it also provides one between them that is selected by default. + * Getting the selected login mechanism with "selectMechanism" method. + +In future commits the "mechanisms-selected" signal and the "selectMechanism" +method of UserVerifier will be implemented. + +There are some mechanisms that are not selectable but run in the +background, e.g. fingerprint. These ones won't be at _authMenuButton but +at the future _authIndicatorButton to inform when they are enabled. + +Co-authored-by: Marco Trevisan (Treviño) +--- + js/gdm/authPrompt.js | 22 +++++++++++++++++++++- + js/gdm/loginDialog.js | 43 +++++++++++++++++++++++++++++++++++++++++-- + js/gdm/util.js | 5 +++++ + 3 files changed, 67 insertions(+), 3 deletions(-) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index 0aca776324..74828a2c42 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -49,7 +49,8 @@ var AuthPrompt = GObject.registerClass({ + 'failed': {}, + 'next': {}, + 'prompted': {}, +- 'reset': { param_types: [GObject.TYPE_UINT] }, ++ 'mechanisms-changed': {param_types: [GObject.TYPE_JSOBJECT, GObject.TYPE_JSOBJECT]}, ++ 'reset': {param_types: [GObject.TYPE_UINT]}, + 'verification-complete': {}, + 'loading': {param_types: [GObject.TYPE_BOOLEAN]}, + }, +@@ -84,6 +85,7 @@ var AuthPrompt = GObject.registerClass({ + 'ask-question', this._onAskQuestion.bind(this), + 'show-message', this._onShowMessage.bind(this), + 'show-choice-list', this._onShowChoiceList.bind(this), ++ 'mechanisms-changed', (_, ...args) => this.emit('mechanisms-changed', ...args), + 'verification-failed', this._onVerificationFailed.bind(this), + 'verification-complete', this._onVerificationComplete.bind(this), + 'reset', this._onReset.bind(this), +@@ -775,6 +777,24 @@ var AuthPrompt = GObject.registerClass({ + this.updateSensitivity(false); + } + ++ selectMechanism(mechanism) { ++ const invalidStatus = [ ++ AuthPromptStatus.VERIFICATION_SUCCEEDED, ++ AuthPromptStatus.VERIFICATION_IN_PROGRESS, ++ ]; ++ if (invalidStatus.includes(this.verificationStatus)) ++ return false; ++ ++ const oldPromptStep = this._promptStep; ++ this._promptStep = 0; ++ if (!this._userVerifier.selectMechanism(mechanism)) ++ this._promptStep = oldPromptStep; ++ ++ this._updateCancelButton(); ++ ++ return true; ++ } ++ + reset(params) { + let {reuseEntryText, softReset} = Params.parse(params, { + reuseEntryText: false, +diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js +index c85706f1d4..ab085fae49 100644 +--- a/js/gdm/loginDialog.js ++++ b/js/gdm/loginDialog.js +@@ -36,6 +36,7 @@ const _FADE_ANIMATION_TIME = 250; + const _SCROLL_ANIMATION_TIME = 500; + const _FIXED_TOP_ACTOR_HEIGHT = 400; + const _TIMED_LOGIN_IDLE_THRESHOLD = 5.0; ++const _PRIMARY_LOGIN_METHOD_SECTION_NAME = _('Login Options'); + const _SESSION_TYPE_SECTION_NAME = _('Session Type'); + + var UserListItem = GObject.registerClass({ +@@ -359,6 +360,7 @@ var LoginDialog = GObject.registerClass({ + 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)); ++ this._authPrompt.connect('mechanisms-changed', this._onMechanismsChanged.bind(this)); + this._authPrompt.hide(); + this.add_child(this._authPrompt); + +@@ -441,7 +443,7 @@ var LoginDialog = GObject.registerClass({ + this._authMenuButton = new AuthMenuButton.AuthMenuButton({ + title: _('Login Options'), + iconName: 'cog-wheel-symbolic', +- sectionOrder: [_SESSION_TYPE_SECTION_NAME], ++ sectionOrder: [_PRIMARY_LOGIN_METHOD_SECTION_NAME, _SESSION_TYPE_SECTION_NAME], + }); + this._authMenuButton.updateSensitivity(false); + +@@ -452,7 +454,9 @@ var LoginDialog = GObject.registerClass({ + if (!item) + return; + +- if (sectionName === _SESSION_TYPE_SECTION_NAME) ++ if (sectionName === _PRIMARY_LOGIN_METHOD_SECTION_NAME) ++ this._selectAuthMechanism(item); ++ else if (sectionName === _SESSION_TYPE_SECTION_NAME) + this._greeter.call_select_session_sync(item.id, null); + + this._authMenuButton.closeMenu(); +@@ -485,6 +489,20 @@ var LoginDialog = GObject.registerClass({ + } + } + ++ _selectAuthMechanism(authMechanism) { ++ const oldMechanism = this._selectedAuthMechanism; ++ ++ if (authMechanism === oldMechanism) ++ return; ++ ++ if (!this._authPrompt.selectMechanism(authMechanism)) { ++ this._authMenuButton.setActiveItem(oldMechanism); ++ return; ++ } ++ ++ this._selectedAuthMechanism = authMechanism; ++ } ++ + _getBannerAllocation(dialogBox) { + let actorBox = new Clutter.ActorBox(); + +@@ -872,6 +890,27 @@ var LoginDialog = GObject.registerClass({ + this._authMenuButton.updateReactive(!isLoading); + } + ++ _onMechanismsChanged(_authPrompt, mechanisms, selectedMechanism) { ++ this._authMenuButton.clearItems({ ++ sectionName: _PRIMARY_LOGIN_METHOD_SECTION_NAME, ++ }); ++ ++ if (mechanisms.length === 0) ++ return; ++ ++ for (const m of mechanisms) { ++ if (GdmUtil.isSelectable(m)) { ++ this._authMenuButton.addItem({ ++ sectionName: _PRIMARY_LOGIN_METHOD_SECTION_NAME, ++ ...m, ++ }); ++ } ++ } ++ ++ if (Object.keys(selectedMechanism).length > 0) ++ this._authMenuButton.setActiveItem(selectedMechanism); ++ } ++ + _onDefaultSessionChanged(client, sessionId) { + this._authMenuButton.setActiveItem({ + sectionName: _SESSION_TYPE_SECTION_NAME, +diff --git a/js/gdm/util.js b/js/gdm/util.js +index 691442ad90..0ff74b4fa1 100644 +--- a/js/gdm/util.js ++++ b/js/gdm/util.js +@@ -244,6 +244,11 @@ var ShellUserVerifier = class { + this._getUserVerifier(); + } + ++ selectMechanism(mechanism) { ++ // TODO: Implement mechanism selection ++ return false; ++ } ++ + cancel() { + if (this._cancellable) + this._cancellable.cancel(); +-- +2.51.0 + + +From 5794aa112c8fab1ed14fa68758a74e6d4a714aad Mon Sep 17 00:00:00 2001 +From: Ray Strode +Date: Tue, 6 Feb 2024 13:41:39 -0500 +Subject: [PATCH 33/48] unlockDialog: Add _authMenuButton and + _authIndicatorButton + +_authMenuButton is used to select an available auth mechanism from the +unlock screen. It's in the bottom right corner of the screen. + +_authIndicatorButton is used to show indicators for background (non +selectable) auth mechanisms. It's in the bottom left corner of the screen. + +Make _otherUserButton written out, instead of using an icon. And move it to +the bottom right corner of the screen, with _authMenuButton. +Only show _otherUserButton when clock is dismissed and authentication +prompt visible. +--- + js/ui/unlockDialog.js | 157 +++++++++++++++++++++++++++++++++++++----- + 1 file changed, 140 insertions(+), 17 deletions(-) + +diff --git a/js/ui/unlockDialog.js b/js/ui/unlockDialog.js +index 07b2acaf45..306b64b55d 100644 +--- a/js/ui/unlockDialog.js ++++ b/js/ui/unlockDialog.js +@@ -4,6 +4,9 @@ + const { AccountsService, Atk, Clutter, Gdm, Gio, + GnomeDesktop, GLib, GObject, Meta, Shell, St } = imports.gi; + ++const AuthMenuButton = imports.gdm.authMenuButton; ++const GdmConstants = imports.gdm.constants; ++const GdmUtil = imports.gdm.util; + const Background = imports.ui.background; + const Layout = imports.ui.layout; + const Main = imports.ui.main; +@@ -12,6 +15,8 @@ const SwipeTracker = imports.ui.swipeTracker; + + const AuthPrompt = imports.gdm.authPrompt; + ++const PRIMARY_UNLOCK_METHOD_SECTION_NAME = _('Unlock Options'); ++ + // The timeout before going back automatically to the lock screen (in seconds) + const IDLE_TIMEOUT = 2 * 60; + +@@ -404,12 +409,13 @@ class UnlockDialogClock extends St.BoxLayout { + + var UnlockDialogLayout = GObject.registerClass( + class UnlockDialogLayout extends Clutter.LayoutManager { +- _init(stack, notifications, switchUserButton) { ++ _init(stack, notifications, authIndicatorButton, bottomButtonGroup) { + super._init(); + + this._stack = stack; + this._notifications = notifications; +- this._switchUserButton = switchUserButton; ++ this._authIndicatorButton = authIndicatorButton; ++ this._bottomButtonGroup = bottomButtonGroup; + } + + vfunc_get_preferred_width(container, forHeight) { +@@ -469,22 +475,40 @@ class UnlockDialogLayout extends Clutter.LayoutManager { + + this._stack.allocate(actorBox); + +- // Switch User button +- if (this._switchUserButton.visible) { +- let [, , natWidth, natHeight] = +- this._switchUserButton.get_preferred_size(); ++ // Auth Indicator button (left bottom) ++ if (this._authIndicatorButton.visible) { ++ const [, , natWidth, natHeight] = ++ this._authIndicatorButton.get_preferred_size(); + +- const textDirection = this._switchUserButton.get_text_direction(); ++ const textDirection = this._authIndicatorButton.get_text_direction(); + if (textDirection === Clutter.TextDirection.RTL) +- actorBox.x1 = box.x1 + natWidth; ++ actorBox.x1 = box.x2 - natWidth; + else +- actorBox.x1 = box.x2 - (natWidth * 2); ++ actorBox.x1 = box.x1; + +- actorBox.y1 = box.y2 - (natHeight * 2); ++ actorBox.y1 = box.y2 - natHeight; + actorBox.x2 = actorBox.x1 + natWidth; + actorBox.y2 = actorBox.y1 + natHeight; + +- this._switchUserButton.allocate(actorBox); ++ this._authIndicatorButton.allocate(actorBox); ++ } ++ ++ // bottom button group, (has login options and switch user buttons) (right bottom) ++ if (this._bottomButtonGroup.visible) { ++ const [, , natWidth, natHeight] = ++ this._bottomButtonGroup.get_preferred_size(); ++ ++ const textDirection = this._bottomButtonGroup.get_text_direction(); ++ if (textDirection === Clutter.TextDirection.RTL) ++ actorBox.x1 = box.x1; ++ else ++ actorBox.x1 = box.x2 - natWidth; ++ ++ actorBox.y1 = box.y2 - natHeight; ++ actorBox.x2 = actorBox.x1 + natWidth; ++ actorBox.y2 = actorBox.y1 + natHeight; ++ ++ this._bottomButtonGroup.allocate(actorBox); + } + } + }); +@@ -589,19 +613,47 @@ var UnlockDialog = GObject.registerClass({ + this._notificationsBox = new NotificationsBox(); + this._notificationsBox.connect('wake-up-screen', () => this.emit('wake-up-screen')); + ++ this._bottomButtonGroup = new St.BoxLayout({ ++ style_class: 'login-dialog-bottom-button-group', ++ }); ++ + // Switch User button + this._otherUserButton = new St.Button({ + style_class: 'modal-dialog-button button switch-user-button', + accessible_name: _('Log in as another user'), + button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE, + reactive: false, +- opacity: 0, + x_align: Clutter.ActorAlign.END, + y_align: Clutter.ActorAlign.END, +- child: new St.Icon({ icon_name: 'system-users-symbolic' }), ++ label: _('Switch User…'), + }); + this._otherUserButton.set_pivot_point(0.5, 0.5); + this._otherUserButton.connect('clicked', this._otherUserClicked.bind(this)); ++ this._bottomButtonGroup.add_child(this._otherUserButton); ++ ++ // Login Options button ++ this._authMenuButton = new AuthMenuButton.AuthMenuButton({ ++ title: _('Login Options'), ++ iconName: 'cog-wheel-symbolic', ++ }); ++ this._authMenuButton.connect('active-item-changed', () => { ++ const authMechanism = this._authMenuButton.getActiveItem(); ++ if (!authMechanism) ++ return; ++ ++ this._selectAuthMechanism(authMechanism); ++ this._authMenuButton.closeMenu(); ++ }); ++ this._authMenuButton.updateSensitivity(true); ++ this._bottomButtonGroup.add_child(this._authMenuButton); ++ ++ // Auth Indicators ++ this._authIndicatorButton = new AuthMenuButton.AuthMenuButtonIndicator({ ++ title: _('Background Authentication Methods'), ++ animateVisibility: true, ++ }); ++ this._authIndicatorButton.set_pivot_point(0.5, 0.5); ++ this._authIndicatorButton.updateSensitivity(true); + + this._screenSaverSettings = new Gio.Settings({ schema_id: 'org.gnome.desktop.screensaver' }); + +@@ -618,11 +670,13 @@ var UnlockDialog = GObject.registerClass({ + mainBox.add_constraint(new Layout.MonitorConstraint({ primary: true })); + mainBox.add_child(this._stack); + mainBox.add_child(this._notificationsBox); +- mainBox.add_child(this._otherUserButton); ++ mainBox.add_child(this._authIndicatorButton); ++ mainBox.add_child(this._bottomButtonGroup); + mainBox.layout_manager = new UnlockDialogLayout( + this._stack, + this._notificationsBox, +- this._otherUserButton); ++ this._authIndicatorButton, ++ this._bottomButtonGroup); + this.add_child(mainBox); + + this._idleMonitor = Meta.IdleMonitor.get_core(); +@@ -653,6 +707,20 @@ var UnlockDialog = GObject.registerClass({ + return Clutter.EVENT_PROPAGATE; + } + ++ _selectAuthMechanism(authMechanism) { ++ const oldMechanism = this._selectedAuthMechanism; ++ ++ if (authMechanism === oldMechanism) ++ return; ++ ++ if (!this._authPrompt.selectMechanism(authMechanism)) { ++ this._authMenuButton.setActiveItem(oldMechanism); ++ return; ++ } ++ ++ this._selectedAuthMechanism = authMechanism; ++ } ++ + _createBackground(monitorIndex) { + let monitor = Main.layoutManager.monitors[monitorIndex]; + let widget = new St.Widget({ +@@ -709,6 +777,8 @@ var UnlockDialog = GObject.registerClass({ + this._authPrompt.connect('failed', this._fail.bind(this)); + this._authPrompt.connect('cancelled', this._fail.bind(this)); + this._authPrompt.connect('reset', this._onReset.bind(this)); ++ this._authPrompt.connect('loading', this._onLoading.bind(this)); ++ this._authPrompt.connect('mechanisms-changed', this._onMechanismsChanged.bind(this)); + this._promptBox.add_child(this._authPrompt); + } + +@@ -771,6 +841,12 @@ var UnlockDialog = GObject.registerClass({ + reactive: progress > 0, + can_focus: progress > 0, + }); ++ this._authIndicatorButton.set({ ++ opacity: 255 * progress, ++ scale_x: FADE_OUT_SCALE + (1 - FADE_OUT_SCALE) * progress, ++ scale_y: FADE_OUT_SCALE + (1 - FADE_OUT_SCALE) * progress, ++ }); ++ this._updateUserSwitchVisibility(); + + const { scaleFactor } = St.ThemeContext.get_for_stage(global.stage); + +@@ -788,7 +864,7 @@ var UnlockDialog = GObject.registerClass({ + translation_y: -FADE_OUT_TRANSLATION * progress * scaleFactor, + }); + +- this._otherUserButton.set({ ++ this._bottomButtonGroup.set({ + opacity: 255 * progress, + scale_x: FADE_OUT_SCALE + (1 - FADE_OUT_SCALE) * progress, + scale_y: FADE_OUT_SCALE + (1 - FADE_OUT_SCALE) * progress, +@@ -812,6 +888,52 @@ var UnlockDialog = GObject.registerClass({ + this._authPrompt.begin({ userName }); + } + ++ _onLoading(_authPrompt, isLoading) { ++ this._authMenuButton.updateReactive(!isLoading); ++ } ++ ++ _onMechanismsChanged(_authPrompt, mechanisms, selectedMechanism) { ++ this._authMenuButton.clearItems({ ++ sectionName: PRIMARY_UNLOCK_METHOD_SECTION_NAME, ++ }); ++ ++ this._authIndicatorButton.clearItems(); ++ ++ if (mechanisms.length === 0) ++ return; ++ ++ for (const m of mechanisms) { ++ if (GdmUtil.isSelectable(m)) { ++ this._authMenuButton.addItem({ ++ sectionName: PRIMARY_UNLOCK_METHOD_SECTION_NAME, ++ ...m, ++ }); ++ } else { ++ this._authIndicatorButton.addItem({ ++ iconName: GdmUtil.getNonSelectableIconName(m), ++ description: this._getUnlockDescription(m), ++ ...m, ++ }); ++ } ++ } ++ ++ if (Object.keys(selectedMechanism).length > 0) ++ this._authMenuButton.setActiveItem(selectedMechanism); ++ ++ this._authIndicatorButton.updateDescriptionLabel(); ++ } ++ ++ _getUnlockDescription(mechanism) { ++ // This is only used for non selectable mechanisms. ++ // Currently only fingerprint is non selectable ++ switch (mechanism.role) { ++ case GdmConstants.FINGERPRINT_ROLE_NAME: ++ return _('Unlock with fingerprint'); ++ default: ++ throw new Error(`Failed getting unlock description: ${mechanism.role}`); ++ } ++ } ++ + _escape() { + if (this._authPrompt && this.allowCancel) + this._authPrompt.cancel(); +@@ -893,7 +1015,8 @@ var UnlockDialog = GObject.registerClass({ + + _updateUserSwitchVisibility() { + this._otherUserButton.visible = this._userManager.can_switch() && +- this._screenSaverSettings.get_boolean('user-switch-enabled'); ++ this._screenSaverSettings.get_boolean('user-switch-enabled') && ++ this._promptBox.visible; + } + + cancel() { +-- +2.51.0 + + +From 3725f050c3be47873916e27a5ddee471b16d3764 Mon Sep 17 00:00:00 2001 +From: Ray Strode +Date: Tue, 3 Dec 2024 07:39:32 -0500 +Subject: [PATCH 34/48] unlockDialog: Update hint text based on mockup + +This considers future mechanisms which might be the default ones, i.e. +smartcard and passkey. And have special hint texts. +--- + js/ui/unlockDialog.js | 14 +++++++++++--- + 1 file changed, 11 insertions(+), 3 deletions(-) + +diff --git a/js/ui/unlockDialog.js b/js/ui/unlockDialog.js +index 306b64b55d..db6f1d4ba1 100644 +--- a/js/ui/unlockDialog.js ++++ b/js/ui/unlockDialog.js +@@ -393,9 +393,17 @@ class UnlockDialogClock extends St.BoxLayout { + } + + _updateHint() { +- this._hint.text = this._seat.touch_mode +- ? _('Swipe up to unlock') +- : _('Click or press a key to unlock'); ++ const authMechanism = this._selectedAuthMechanism; ++ let text; ++ ++ if (authMechanism?.role === GdmConstants.SMARTCARD_ROLE_NAME) ++ text = _('Insert smartcard'); ++ else if (this._seat.touch_mode) ++ text = _('Swipe up'); ++ else ++ text = _('Click or press a key'); ++ ++ this._hint.text = text; + } + + _onDestroy() { +-- +2.51.0 + + +From 01988c6b13a40b2383edaf3cca1be0bc402e3102 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Tue, 14 Oct 2025 19:19:15 +0200 +Subject: [PATCH 35/48] gdm/util: Increase time of messages based on new + environment variable + +called 'GDM_MESSAGE_TIME_MULTIPLIER'. This is used for testing +purposes. When no set, the multiplier is 1 which does nothing. +--- + js/gdm/util.js | 7 ++++++- + 1 file changed, 6 insertions(+), 1 deletion(-) + +diff --git a/js/gdm/util.js b/js/gdm/util.js +index 0ff74b4fa1..ae35e5bc5c 100644 +--- a/js/gdm/util.js ++++ b/js/gdm/util.js +@@ -46,6 +46,11 @@ var DISABLE_USER_LIST_KEY = 'disable-user-list'; + + // Give user 48ms to read each character of a PAM message + var USER_READ_TIME = 48; ++const MESSAGE_TIME_MULTIPLIER = (() => { ++ const value = Number.parseFloat(GLib.getenv('GDM_MESSAGE_TIME_MULTIPLIER')); ++ return Number.isFinite(value) && value > 0 ? value : 1; ++})(); ++ + const FINGERPRINT_SERVICE_PROXY_TIMEOUT = 5000; + const FINGERPRINT_ERROR_TIMEOUT_WAIT = 15; + +@@ -323,7 +328,7 @@ var ShellUserVerifier = class { + return 0; + + // We probably could be smarter here +- return message.length * USER_READ_TIME; ++ return message.length * USER_READ_TIME * MESSAGE_TIME_MULTIPLIER; + } + + finishMessageQueue() { +-- +2.51.0 + + +From 8329dbcdeaa0f5dc1b3e37a994d7396a3b508ce1 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Mon, 15 Sep 2025 17:13:17 +0200 +Subject: [PATCH 36/48] gdm/util: Allow null _hold and don't recreate dummy + holds + +_hold property is used to inform the caller of begin method (authPrompt) +when it's started the verification (calling acquire) and when it +finished starting it, either successfully or with an error (calling +release). + +There's no need to create dummy holds that won't be used anywhere +externally. + +In the next commits the use of _hold will be simplified considerably, +but for now, just accept getting null and dont create dummy ones. +--- + js/gdm/authPrompt.js | 6 +----- + js/gdm/util.js | 15 ++++++--------- + 2 files changed, 7 insertions(+), 14 deletions(-) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index 74828a2c42..a7ec1dcda4 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -868,11 +868,7 @@ var AuthPrompt = GObject.registerClass({ + + this.updateSensitivity({sensitive: false}); + +- let hold = params.hold; +- if (!hold) +- hold = new Batch.Hold(); +- +- this._userVerifier.begin(params.userName, hold); ++ this._userVerifier.begin(params.userName, params.hold); + this.verificationStatus = AuthPromptStatus.VERIFYING; + } + +diff --git a/js/gdm/util.js b/js/gdm/util.js +index ae35e5bc5c..e862e2c0e7 100644 +--- a/js/gdm/util.js ++++ b/js/gdm/util.js +@@ -519,8 +519,6 @@ var ShellUserVerifier = class { + + if (this._userVerifier && + !this._activeServices.has(Constants.FINGERPRINT_SERVICE_NAME)) { +- if (!this._hold?.isAcquired()) +- this._hold = new Batch.Hold(); + await this._maybeStartFingerprintVerification(); + } + } +@@ -583,7 +581,7 @@ var ShellUserVerifier = class { + + _reportInitError(where, error, serviceName) { + logError(error, where); +- this._hold.release(); ++ this._hold?.release(); + + this._queueMessage(serviceName, _('Authentication error'), MessageType.ERROR); + this._failCounter++; +@@ -619,7 +617,7 @@ var ShellUserVerifier = class { + this.reauthenticating = true; + this._connectSignals(); + this._beginVerification(); +- this._hold.release(); ++ this._hold?.release(); + } + + async _getUserVerifier() { +@@ -641,7 +639,7 @@ var ShellUserVerifier = class { + + this._connectSignals(); + this._beginVerification(); +- this._hold.release(); ++ this._hold?.release(); + } + + _connectSignals() { +@@ -760,7 +758,7 @@ var ShellUserVerifier = class { + } + + async _startService(serviceName) { +- this._hold.acquire(); ++ this._hold?.acquire(); + try { + this._activeServices.add(serviceName); + if (this._userName) { +@@ -776,7 +774,7 @@ var ShellUserVerifier = class { + return; + if (!this.serviceIsForeground(serviceName)) { + logError(e, 'Failed to start %s for %s'.format(serviceName, this._userName)); +- this._hold.release(); ++ this._hold?.release(); + return; + } + this._reportInitError(this._userName +@@ -785,7 +783,7 @@ var ShellUserVerifier = class { + serviceName); + return; + } +- this._hold.release(); ++ this._hold?.release(); + } + + _beginVerification() { +@@ -911,7 +909,6 @@ var ShellUserVerifier = class { + } + + _retry(serviceName) { +- this._hold = new Batch.Hold(); + this._connectSignals(); + this._startService(serviceName); + } +-- +2.51.0 + + +From 43d82e9611eb899c345b972633ad279a4b71d6df Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Wed, 20 Aug 2025 10:54:33 +0200 +Subject: [PATCH 37/48] misc: Add FingerprintManager + +Move fingerprint bits to new fingerprintManager class. + +This helps keeping util simpler and tries to encapsulate fingerprint +functionality inside fingerprintManager. + +This will be improved in the next commits when moving fingerprint +authentication to a specific class instead of util. +--- + js/gdm/util.js | 159 ++++++++-------------------------- + js/js-resources.gresource.xml | 1 + + js/misc/fingerprintManager.js | 138 +++++++++++++++++++++++++++++ + 3 files changed, 174 insertions(+), 124 deletions(-) + create mode 100644 js/misc/fingerprintManager.js + +diff --git a/js/gdm/util.js b/js/gdm/util.js +index e862e2c0e7..1176f77500 100644 +--- a/js/gdm/util.js ++++ b/js/gdm/util.js +@@ -7,6 +7,7 @@ const Signals = imports.signals; + + const Batch = imports.gdm.batch; + const Constants = imports.gdm.constants; ++const {FingerprintManager, FingerprintReaderType} = imports.misc.fingerprintManager; + const OVirt = imports.gdm.oVirt; + const Vmware = imports.gdm.vmware; + const Main = imports.ui.main; +@@ -14,11 +15,6 @@ const { loadInterfaceXML } = imports.misc.fileUtils; + const Params = imports.misc.params; + const SmartcardManager = imports.misc.smartcardManager; + +-const FprintManagerInfo = Gio.DBusInterfaceInfo.new_for_xml( +- loadInterfaceXML('net.reactivated.Fprint.Manager')); +-const FprintDeviceInfo = Gio.DBusInterfaceInfo.new_for_xml( +- loadInterfaceXML('net.reactivated.Fprint.Device')); +- + Gio._promisify(Gdm.Client.prototype, + 'open_reauthentication_channel', 'open_reauthentication_channel_finish'); + Gio._promisify(Gdm.Client.prototype, +@@ -62,12 +58,6 @@ var MessageType = { + ERROR: 3, + }; + +-const FingerprintReaderType = { +- NONE: 0, +- PRESS: 1, +- SWIPE: 2, +-}; +- + function fadeInActor(actor) { + if (actor.opacity == 255 && actor.visible) + return null; +@@ -187,6 +177,7 @@ var ShellUserVerifier = class { + this._defaultService = null; + this._preemptingService = null; + this._fingerprintReaderType = FingerprintReaderType.NONE; ++ this._fingerprintReaderFound = false; + + this._messageQueue = []; + this._messageQueueTimeoutId = 0; +@@ -238,8 +229,7 @@ var ShellUserVerifier = class { + this._userName = userName; + this.reauthenticating = false; + +- this._checkForFingerprintReader().catch(e => +- this._handleFingerprintError(e)); ++ this._fingerprintManager?.checkReaderType(this._cancellable); + + // If possible, reauthenticate an already running session, + // so any session specific credentials get updated appropriately +@@ -297,6 +287,7 @@ var ShellUserVerifier = class { + this._smartcardManager?.disconnect(this._smartcardRemovedId); + this._smartcardManager = null; + ++ this._fingerprintManager?.disconnectObject(this); + this._fingerprintManager = null; + + for (let service in this._credentialManagers) { +@@ -414,121 +405,39 @@ var ShellUserVerifier = class { + } + + async _initFingerprintManager() { +- if (this._fprintManager) +- return; +- +- const fprintManager = new Gio.DBusProxy({ +- g_connection: Gio.DBus.system, +- g_name: 'net.reactivated.Fprint', +- g_object_path: '/net/reactivated/Fprint/Manager', +- g_interface_name: FprintManagerInfo.name, +- g_interface_info: FprintManagerInfo, +- g_flags: Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES | +- Gio.DBusProxyFlags.DO_NOT_AUTO_START_AT_CONSTRUCTION | +- Gio.DBusProxyFlags.DO_NOT_CONNECT_SIGNALS, +- }); +- +- try { +- if (!this._getDetectedDefaultService()) { +- // Other authentication methods would have already been detected by +- // now as possibilities if they were available. +- // If we're here it means that FINGERPRINT_AUTHENTICATION_KEY is +- // true and so fingerprint authentication is our last potential +- // option, so go ahead a synchronously look for a fingerprint device +- // during startup or default service update. +- fprintManager.init(null); +- // Do not wait too much for fprintd to reply, as in case it hangs +- // we should fail early without having the shell to misbehave +- fprintManager.set_default_timeout(FINGERPRINT_SERVICE_PROXY_TIMEOUT); +- +- const result = fprintManager.call_sync( +- 'GetDefaultDevice', +- null, +- Gio.DBusCallFlags.NONE, +- FINGERPRINT_SERVICE_PROXY_TIMEOUT, +- null); +- const [devicePath] = result.deep_unpack(); +- this._fprintManager = fprintManager; +- +- const fprintDeviceProxy = this._getFingerprintDeviceProxy(devicePath); +- fprintDeviceProxy.init(null); +- this._setFingerprintReaderType(fprintDeviceProxy['scan-type']); +- } else { +- // Ensure fingerprint service starts, but do not wait for it +- const cancellable = this._cancellable; +- await fprintManager.init_async(GLib.PRIORITY_DEFAULT, cancellable); +- await this._updateFingerprintReaderType(fprintManager, cancellable); +- this._fprintManager = fprintManager; +- } +- } catch (e) { +- this._handleFingerprintError(e); +- } +- } +- +- _getFingerprintDeviceProxy(devicePath) { +- return new Gio.DBusProxy({ +- g_connection: Gio.DBus.system, +- g_name: 'net.reactivated.Fprint', +- g_object_path: devicePath, +- g_interface_name: FprintDeviceInfo.name, +- g_interface_info: FprintDeviceInfo, +- g_flags: Gio.DBusProxyFlags.DO_NOT_CONNECT_SIGNALS, +- }); +- } +- +- _handleFingerprintError(e) { +- this._fingerprintReaderType = FingerprintReaderType.NONE; +- +- if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) +- return; +- if (e.matches(Gio.DBusError, Gio.DBusError.SERVICE_UNKNOWN)) ++ if (this._fingerprintManager) + return; + +- if (Gio.DBusError.is_remote_error(e) && +- Gio.DBusError.get_remote_error(e) === +- 'net.reactivated.Fprint.Error.NoSuchDevice') +- return; +- +- logError(e, 'Failed to interact with fprintd service'); +- } +- +- async _checkForFingerprintReader() { +- if (!this._fprintManager) { +- this._updateDefaultService(); +- return; ++ this._fingerprintManager = new FingerprintManager(this._cancellable); ++ this._fingerprintManager.connectObject( ++ 'reader-type-changed', () => this._onFingerprintReaderTypeChanged(), ++ this); ++ ++ if (!this._getDetectedDefaultService()) { ++ // Other authentication methods would have already been detected by ++ // now as possibilities if they were available. ++ // If we're here it means that FINGERPRINT_AUTHENTICATION_KEY is ++ // true and so fingerprint authentication is our last potential ++ // option, so go ahead a synchronously look for a fingerprint device ++ // during startup or default service update. ++ // Do not wait too much for fprintd to reply, as in case it hangs ++ // we should fail early without having the shell to misbehave ++ this._fingerprintManager.setDefaultTimeout(FINGERPRINT_SERVICE_PROXY_TIMEOUT); ++ await this._fingerprintManager.checkReaderType(this._cancellable); ++ } else { ++ // Ensure fingerprint service starts, but do not wait for it ++ this._fingerprintManager.checkReaderType(this._cancellable); + } +- +- if (this._fingerprintReaderType !== FingerprintReaderType.NONE) +- return; +- +- await this._updateFingerprintReaderType(this._fprintManager, this._cancellable); + } + +- async _updateFingerprintReaderType(fprintManager, cancellable) { +- const result = await fprintManager.call( +- 'GetDefaultDevice', +- null, +- Gio.DBusCallFlags.NONE, +- -1, +- cancellable); +- const [devicePath] = result.deep_unpack(); +- const fprintDeviceProxy = this._getFingerprintDeviceProxy(devicePath); +- await fprintDeviceProxy.init_async(GLib.PRIORITY_DEFAULT, cancellable); +- this._setFingerprintReaderType(fprintDeviceProxy['scan-type']); ++ _onFingerprintReaderTypeChanged() { ++ this._fingerprintReaderType = this._fingerprintManager.readerType; ++ this._fingerprintReaderFound = this._fingerprintManager.readerFound; + this._updateDefaultService(); + + if (this._userVerifier && +- !this._activeServices.has(Constants.FINGERPRINT_SERVICE_NAME)) { +- await this._maybeStartFingerprintVerification(); +- } +- } +- +- _setFingerprintReaderType(fprintDeviceType) { +- this._fingerprintReaderType = +- FingerprintReaderType[fprintDeviceType.toUpperCase()]; +- +- if (this._fingerprintReaderType === undefined) +- throw new Error(`Unexpected fingerprint device type '${fprintDeviceType}'`); ++ !this._activeServices.has(Constants.FINGERPRINT_SERVICE_NAME)) ++ this._maybeStartFingerprintVerification(); + } + + _onCredentialManagerAuthenticated(credentialManager, _token) { +@@ -693,7 +602,7 @@ var ShellUserVerifier = class { + } + + serviceIsFingerprint(serviceName) { +- return this._fingerprintReaderType !== FingerprintReaderType.NONE && ++ return this._fingerprintReaderFound && + serviceName === Constants.FINGERPRINT_SERVICE_NAME; + } + +@@ -706,9 +615,11 @@ var ShellUserVerifier = class { + let needsReset = false; + + if (this._settings.get_boolean(FINGERPRINT_AUTHENTICATION_KEY)) { +- this._initFingerprintManager().catch(logError); ++ this._initFingerprintManager(); + } else if (this._fingerprintManager) { ++ this._fingerprintManager.disconnectObject(this); + this._fingerprintManager = null; ++ this._fingerprintReaderFound = false; + this._fingerprintReaderType = FingerprintReaderType.NONE; + + if (this._activeServices.has(Constants.FINGERPRINT_SERVICE_NAME)) +@@ -737,7 +648,7 @@ var ShellUserVerifier = class { + return Constants.PASSWORD_SERVICE_NAME; + else if (this._smartcardManager) + return Constants.SMARTCARD_SERVICE_NAME; +- else if (this._fingerprintReaderType !== FingerprintReaderType.NONE) ++ else if (this._fingerprintReaderFound) + return Constants.FINGERPRINT_SERVICE_NAME; + return null; + } +@@ -793,7 +704,7 @@ var ShellUserVerifier = class { + + async _maybeStartFingerprintVerification() { + if (this._userName && +- this._fingerprintReaderType !== FingerprintReaderType.NONE && ++ this._fingerprintReaderFound && + !this.serviceIsForeground(Constants.FINGERPRINT_SERVICE_NAME)) + await this._startService(Constants.FINGERPRINT_SERVICE_NAME); + } +diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml +index 105076b40e..ad4057d585 100644 +--- a/js/js-resources.gresource.xml ++++ b/js/js-resources.gresource.xml +@@ -15,6 +15,7 @@ + + misc/config.js + misc/extensionUtils.js ++ misc/fingerprintManager.js + misc/fileUtils.js + misc/gnomeSession.js + misc/history.js +diff --git a/js/misc/fingerprintManager.js b/js/misc/fingerprintManager.js +new file mode 100644 +index 0000000000..6ef2d68d3a +--- /dev/null ++++ b/js/misc/fingerprintManager.js +@@ -0,0 +1,138 @@ ++import Gio from 'gi://Gio'; ++import GLib from 'gi://GLib'; ++import GObject from 'gi://GObject'; ++ ++import {loadInterfaceXML} from './fileUtils.js'; ++ ++const FprintManagerInfo = Gio.DBusInterfaceInfo.new_for_xml( ++ loadInterfaceXML('net.reactivated.Fprint.Manager')); ++const FprintDeviceInfo = Gio.DBusInterfaceInfo.new_for_xml( ++ loadInterfaceXML('net.reactivated.Fprint.Device')); ++ ++export const FingerprintReaderType = { ++ NONE: 0, ++ PRESS: 1, ++ SWIPE: 2, ++}; ++ ++let _fingerprintManager = null; ++ ++/** ++ * @returns {FingerprintManager} ++ */ ++export function getFingerprintManager() { ++ if (_fingerprintManager == null) ++ _fingerprintManager = new FingerprintManager(); ++ ++ return _fingerprintManager; ++} ++ ++class FingerprintManager extends GObject.Object { ++ static [GObject.properties] = { ++ 'reader-type': GObject.ParamSpec.uint( ++ 'reader-type', null, null, ++ GObject.ParamFlags.READWRITE, ++ FingerprintReaderType.NONE, FingerprintReaderType.SWIPE, ++ FingerprintReaderType.NONE), ++ }; ++ ++ static [GObject.signals] = { ++ 'reader-type-changed': {}, ++ }; ++ ++ static { ++ GObject.registerClass(this); ++ } ++ ++ constructor() { ++ super(); ++ ++ this._fingerprintManagerProxy = new Gio.DBusProxy({ ++ g_connection: Gio.DBus.system, ++ g_name: 'net.reactivated.Fprint', ++ g_object_path: '/net/reactivated/Fprint/Manager', ++ g_interface_name: FprintManagerInfo.name, ++ g_interface_info: FprintManagerInfo, ++ g_flags: Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES | ++ Gio.DBusProxyFlags.DO_NOT_AUTO_START_AT_CONSTRUCTION | ++ Gio.DBusProxyFlags.DO_NOT_CONNECT_SIGNALS, ++ }); ++ ++ this._initFingerprintManagerProxy(); ++ } ++ ++ get readerFound() { ++ return this.readerType !== FingerprintReaderType.NONE; ++ } ++ ++ setDefaultTimeout(timeout) { ++ this._fingerprintManagerProxy.set_default_timeout(timeout); ++ } ++ ++ async checkReaderType(cancellable = null) { ++ try { ++ // Wrappers don't support null cancellable, so let's ignore it in case ++ const args = cancellable ? [cancellable] : []; ++ const [devicePath] = ++ await this._fingerprintManagerProxy.GetDefaultDeviceAsync(...args); ++ ++ const fprintDeviceProxy = this._getFprintDeviceProxy(devicePath); ++ await fprintDeviceProxy.init_async(GLib.PRIORITY_DEFAULT, cancellable ?? null); ++ ++ const fingerprintReaderKey = fprintDeviceProxy['scan-type'].toUpperCase(); ++ const fingerprintReaderType = FingerprintReaderType[fingerprintReaderKey]; ++ this._setFingerprintReaderType(fingerprintReaderType); ++ } catch (e) { ++ if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) ++ return; ++ this._handleFingerprintError(e); ++ } ++ } ++ ++ async _initFingerprintManagerProxy() { ++ try { ++ await this._fingerprintManagerProxy.init_async( ++ GLib.PRIORITY_DEFAULT, null); ++ await this.checkReaderType(); ++ } catch (e) { ++ this._handleFingerprintError(e); ++ } ++ } ++ ++ _getFprintDeviceProxy(devicePath) { ++ return new Gio.DBusProxy({ ++ g_connection: Gio.DBus.system, ++ g_name: 'net.reactivated.Fprint', ++ g_object_path: devicePath, ++ g_interface_name: FprintDeviceInfo.name, ++ g_interface_info: FprintDeviceInfo, ++ g_flags: Gio.DBusProxyFlags.DO_NOT_CONNECT_SIGNALS, ++ }); ++ } ++ ++ _setFingerprintReaderType(fingerprintReaderType) { ++ if (fingerprintReaderType === undefined) ++ throw new Error(`Unexpected fingerprint device type '${fingerprintReaderType}'`); ++ ++ if (this.readerType === fingerprintReaderType) ++ return; ++ ++ this.readerType = fingerprintReaderType; ++ this.emit('reader-type-changed'); ++ } ++ ++ _handleFingerprintError(e) { ++ this._setFingerprintReaderType(FingerprintReaderType.NONE); ++ ++ if (e instanceof GLib.Error) { ++ if (e.matches(Gio.DBusError, Gio.DBusError.SERVICE_UNKNOWN)) ++ return; ++ if (Gio.DBusError.is_remote_error(e) && ++ Gio.DBusError.get_remote_error(e) === ++ 'net.reactivated.Fprint.Error.NoSuchDevice') ++ return; ++ } ++ ++ logError(e, 'Failed to interact with fprintd service'); ++ } ++} +-- +2.51.0 + + +From 17023fb0e31b0b18ef5cc47eeca2f08f38132ee7 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Tue, 23 Sep 2025 16:45:59 +0200 +Subject: [PATCH 38/48] misc: Add PasskeyDeviceManager + +This utility will be used in the next commits, when passkey authentication +is implemented, to detect when a passkey has been inserted or removed. + +To detect if a sysfs device is a passkey (fido2), it's been used the +implementation of systemd in fido_id_desc.c. +--- + js/js-resources.gresource.xml | 1 + + js/misc/passkeyDeviceManager.js | 69 +++++++++++++++++++++++++++++++++ + 2 files changed, 70 insertions(+) + create mode 100644 js/misc/passkeyDeviceManager.js + +diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml +index ad4057d585..df6dbf5070 100644 +--- a/js/js-resources.gresource.xml ++++ b/js/js-resources.gresource.xml +@@ -29,6 +29,7 @@ + misc/objectManager.js + misc/params.js + misc/parentalControlsManager.js ++ misc/passkeyDeviceManager.js + misc/permissionStore.js + misc/smartcardManager.js + misc/systemActions.js +diff --git a/js/misc/passkeyDeviceManager.js b/js/misc/passkeyDeviceManager.js +new file mode 100644 +index 0000000000..20799975aa +--- /dev/null ++++ b/js/misc/passkeyDeviceManager.js +@@ -0,0 +1,69 @@ ++import GObject from 'gi://GObject'; ++import GUdev from 'gi://GUdev'; ++ ++let _passkeyDeviceManager = null; ++ ++/** ++ * @returns {PasskeyDeviceManager} ++ */ ++export function getPasskeyDeviceManager() { ++ if (_passkeyDeviceManager == null) ++ _passkeyDeviceManager = new PasskeyDeviceManager(); ++ ++ return _passkeyDeviceManager; ++} ++ ++class PasskeyDeviceManager extends GObject.Object { ++ static [GObject.signals] = { ++ 'passkey-inserted': {param_types: [GObject.TYPE_JSOBJECT]}, ++ 'passkey-removed': {param_types: [GObject.TYPE_JSOBJECT]}, ++ }; ++ ++ static { ++ GObject.registerClass(this); ++ } ++ ++ constructor() { ++ super(); ++ ++ this._insertedPasskeys = new Map(); ++ this._udevClient = new GUdev.Client({subsystems: ['hidraw']}); ++ ++ this._onLoaded(); ++ } ++ ++ get hasInsertedPasskeys() { ++ return this._insertedPasskeys.size > 0; ++ } ++ ++ _onLoaded() { ++ this._udevClient.query_by_subsystem('hidraw') ++ .forEach(d => this._addPasskey(d)); ++ ++ this._udevClient.connect('uevent', (_, action, device) => { ++ if (action === 'add') ++ this._addPasskey(device); ++ else if (action === 'remove') ++ this._removePasskey(device); ++ }); ++ } ++ ++ _addPasskey(device) { ++ const sysfsPath = device.get_sysfs_path(); ++ const isFido = device.get_property_as_int('ID_FIDO_TOKEN') === 1; ++ if (!isFido || this._insertedPasskeys.has(sysfsPath)) ++ return; ++ ++ this._insertedPasskeys.set(sysfsPath, device); ++ this.emit('passkey-inserted', device); ++ } ++ ++ _removePasskey(device) { ++ const sysfsPath = device.get_sysfs_path(); ++ if (!this._insertedPasskeys.has(sysfsPath)) ++ return; ++ ++ this._insertedPasskeys.delete(sysfsPath); ++ this.emit('passkey-removed', device); ++ } ++} +-- +2.51.0 + + +From 9d32a080ea06fcdc7b653b7984d80ee7dfe62fcb Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Tue, 10 Mar 2026 14:27:50 +0100 +Subject: [PATCH 39/48] misc/smartcardManager: Skip login_token aliases to + avoid duplicate events + +Tokens with '/login_token' paths are aliases mirroring already +connected tokens. Connecting to both causes duplicate events. +--- + js/misc/smartcardManager.js | 6 ++++++ + 1 file changed, 6 insertions(+) + +diff --git a/js/misc/smartcardManager.js b/js/misc/smartcardManager.js +index 26f9f5aaa9..cd95741b6c 100644 +--- a/js/misc/smartcardManager.js ++++ b/js/misc/smartcardManager.js +@@ -66,6 +66,12 @@ var SmartcardManager = class { + } + + _addToken(token) { ++ // This is an alias, mirroring the real token. ++ // Skip it to avoid duplicate events ++ const objectPath = token.get_object_path(); ++ if (objectPath.endsWith('/login_token')) ++ return; ++ + this._updateToken(token); + + token.connect('g-properties-changed', (proxy, properties) => { +-- +2.51.0 + + +From fe23b7b14fa5c4c2f42e48603b6030e3e7e5f51d Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Mon, 18 Aug 2025 12:08:21 +0200 +Subject: [PATCH 40/48] gdm: Add AuthServices +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +util.js had become overly complex, handling too many decoupled responsibilities. +To improve readability and maintainability, it's been introduced a dedicated +class to manage authentication services and their respective logic that +was previously in util.js. + +AuthServices is the base class that defines the common interface (signals +like "ask-question", "verification-complete", "mechanisms-changed", etc.) and +shared logic for authentication handling, while its childs like AuthServicesLegacy +are derived implementations that handle different authentication backends. + +The Legacy naming in AuthServicesLegacy distinguishes the traditional PAM-based +architecture (rigid PAM messages and different services) from the upcoming +unified approach (JSON PAM messages and unified single service). It is important +to note that it's not deprecated; it remains the primary handler for standard GDM/PAM +services (gdm-password, gdm-smartcard, gdm-fingerprint). + +The default workflow of authServices is: +1. beginVerification method is called with userName (optional) and + new userVerifierProxies. +2. 'mechanisms-changed' signal is emitted with the current enabled + mechanisms and a default selected one. +3. Calling selectMechanism method makes the selected authentication + mechanism start (will do nothing if mechanism was already selected). +4. When a mechanism is started, depending on the mechanism, it will + emit signals like 'ask-question', 'show-choice-list'..., and receive + feedback with methods like answerQuery or selectChoice. +5. After the back and forth of signals and methods, the authentication + can succeed ('verification-complete') or fail ('verification-failed'). +6. If the verification fails, authServices is in charge of requesting a reset, + either soft (reusing the username and the selected mechanism) or hard + (full reset). + +The main properties of authServices are: +* _enabledRoles: Contains the roles that are currently enabled in GDM settings. + Valid roles are defined in constants.js (PASSWORD_ROLE_NAME, SMARTCARD_ROLE_NAME...). + This determines which mechanisms are included in _enabledMechanisms. +* _enabledMechanisms: Contains the mechanisms that are currently enabled. These + are the ones sent on 'mechanisms-changed'. +* _selectedMechanism: It's the currently selected mechanism. Similar of + what foregroundService was in UserVerifier. It must be one mechanism of the + ones in _enabledMechanisms. +* _userVerifier, and its siblings: They're the proxies used to have PAM + conversations with different PAM services trough the GDM API. + This is the interface that requests a password, a selection from a choice + list, etc. and gets the responses. + +The authentication of password, smartcard and fingerprint now is +implemented in the child class called AuthServicesLegacy. +1. It implements a hardcoded array of mechanisms based on roles (password, + smartcard and fingerprint). +2. Each mechanism is driven by a different PAM service, and _roleToService + is used to decide which service to use when starting a selected mechanism. +3. When a mechanism is selected, it's emited 'reset' signal with softReset: true + which will start a conversation reusing the username and the selected + mechanism. +4. Fingerprint mechanism is non selectable, so it will be started + automatically on beginVerification, if it's enabled. +5. Authentication services from credentialManagers are also supported, they + won't be shown in the mechanisms list, but they will be automatically started + if they have preauthenticated credentials. + +Based on the previous work done by: + - Marco Trevisan (Treviño) + - Ray Strode +--- + js/gdm/authPrompt.js | 39 +- + js/gdm/authServices.js | 468 +++++++++++++++++++++ + js/gdm/authServicesLegacy.js | 334 +++++++++++++++ + js/gdm/util.js | 742 ++++++++-------------------------- + js/js-resources.gresource.xml | 2 + + po/POTFILES.in | 1 + + 6 files changed, 988 insertions(+), 598 deletions(-) + create mode 100644 js/gdm/authServices.js + create mode 100644 js/gdm/authServicesLegacy.js + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index a7ec1dcda4..5a92c725b5 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -6,7 +6,6 @@ const { Clutter, Gio, GLib, GObject, Graphene, Meta, Pango, Shell, St } = import + const Animation = imports.ui.animation; + const AuthList = imports.gdm.authList; + const Batch = imports.gdm.batch; +-const Constants = imports.gdm.constants; + const GdmUtil = imports.gdm.util; + const OVirt = imports.gdm.oVirt; + const Vmware = imports.gdm.vmware; +@@ -89,10 +88,7 @@ var AuthPrompt = GObject.registerClass({ + 'verification-failed', this._onVerificationFailed.bind(this), + 'verification-complete', this._onVerificationComplete.bind(this), + 'reset', this._onReset.bind(this), +- 'smartcard-status-changed', this._onSmartcardStatusChanged.bind(this), +- 'credential-manager-authenticated', this._onCredentialManagerAuthenticated.bind(this), + this); +- this.smartcardDetected = this._userVerifier.smartcardDetected; + + this.connect('destroy', this._onDestroy.bind(this)); + +@@ -444,31 +440,6 @@ var AuthPrompt = GObject.registerClass({ + this.emit('prompted'); + } + +- _onCredentialManagerAuthenticated() { +- if (this.verificationStatus != AuthPromptStatus.VERIFICATION_SUCCEEDED) +- this.reset(); +- } +- +- _onSmartcardStatusChanged() { +- this.smartcardDetected = this._userVerifier.smartcardDetected; +- +- // Most of the time we want to reset if the user inserts or removes +- // a smartcard. Smartcard insertion "preempts" what the user was +- // doing, and smartcard removal aborts the preemption. +- // The exceptions are: 1) Don't reset on smartcard insertion if we're already verifying +- // with a smartcard +- // 2) Don't reset if we've already succeeded at verification and +- // the user is getting logged in. +- if (this._userVerifier.serviceIsDefault(Constants.SMARTCARD_SERVICE_NAME) && +- (this.verificationStatus === AuthPromptStatus.VERIFYING || +- this.verificationStatus === AuthPromptStatus.VERIFICATION_IN_PROGRESS) && +- this.smartcardDetected) +- return; +- +- if (this.verificationStatus != AuthPromptStatus.VERIFICATION_SUCCEEDED) +- this.reset(); +- } +- + _onShowMessage(_userVerifier, serviceName, message, type) { + this.setMessage(serviceName, message, type); + +@@ -813,8 +784,10 @@ var AuthPrompt = GObject.registerClass({ + this._preemptiveAnswerWatchId = this._idleMonitor.add_idle_watch(500, + this._onUserStoppedTypePreemptiveAnswer.bind(this)); + +- if (this._userVerifier) +- this._userVerifier.cancel(); ++ if (softReset) ++ this._userVerifier?.cancel(); ++ else ++ this._userVerifier?.reset(); + + reuseEntryText = reuseEntryText || this._preemptiveInput; + +@@ -838,9 +811,7 @@ var AuthPrompt = GObject.registerClass({ + if (oldStatus === AuthPromptStatus.VERIFICATION_CANCELLED) + return; + resetType = ResetType.PROVIDE_USERNAME; +- } else if (this._userVerifier.serviceIsForeground(OVirt.SERVICE_NAME) || +- this._userVerifier.serviceIsForeground(Vmware.SERVICE_NAME) || +- this._userVerifier.serviceIsForeground(GdmUtil.SMARTCARD_SERVICE_NAME)) { ++ } 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..d522ee352e +--- /dev/null ++++ b/js/gdm/authServices.js +@@ -0,0 +1,468 @@ ++// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- ++ ++import * as FingerprintManager from '../misc/fingerprintManager.js'; ++import * as Params from '../misc/params.js'; ++import {registerDestroyableType} from '../misc/signalTracker.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'; ++import Gio from 'gi://Gio'; ++import GObject from 'gi://GObject'; ++ ++Gio._promisify(Gdm.Client.prototype, 'open_reauthentication_channel'); ++Gio._promisify(Gdm.Client.prototype, 'get_user_verifier'); ++Gio._promisify(Gdm.UserVerifierProxy.prototype, 'call_begin_verification_for_user'); ++Gio._promisify(Gdm.UserVerifierProxy.prototype, 'call_begin_verification'); ++Gio._promisify(Gdm.UserVerifierProxy.prototype, 'call_answer_query'); ++Gio._promisify(Gdm.UserVerifierChoiceListProxy.prototype, 'call_select_choice'); ++ ++export class AuthServices extends GObject.Object { ++ static [GObject.signals] = { ++ 'destroy': {}, ++ 'queue-message': { ++ param_types: [GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_UINT], ++ }, ++ 'queue-priority-message': { ++ param_types: [GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_UINT], ++ }, ++ 'wait-pending-messages': { ++ param_types: [GObject.TYPE_JSOBJECT], ++ }, ++ 'filter-messages': { ++ param_types: [GObject.TYPE_STRING, GObject.TYPE_UINT], ++ }, ++ 'verification-failed': { ++ param_types: [GObject.TYPE_STRING, GObject.TYPE_BOOLEAN], ++ }, ++ 'verification-complete': {}, ++ 'ask-question': { ++ param_types: [GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_BOOLEAN], ++ }, ++ 'reset': { ++ param_types: [GObject.TYPE_JSOBJECT], ++ }, ++ 'show-choice-list': { ++ param_types: [GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_JSOBJECT], ++ }, ++ 'mechanisms-changed': {}, ++ }; ++ ++ static { ++ GObject.registerClass(this); ++ registerDestroyableType(this); ++ } ++ ++ static SupportedRoles = []; ++ static RoleToService = {}; ++ ++ static supportsAny(roles) { ++ return roles.some(r => this.SupportedRoles.includes(r)); ++ } ++ ++ constructor(params) { ++ super(); ++ params = Params.parse(params, { ++ client: null, ++ enabledRoles: [], ++ allowedFailures: 3, ++ reauthOnly: false, ++ }); ++ ++ this._client = params.client; ++ this._enabledRoles = params.enabledRoles; ++ this._allowedFailures = params.allowedFailures; ++ this._reauthOnly = params.reauthOnly; ++ ++ this._failCounter = 0; ++ this._activeServices = new Set(); ++ this._unavailableServices = new Set(); ++ ++ this._cancellable = null; ++ ++ this._connectSmartcardManager(); ++ this._connectFingerprintManager(); ++ } ++ ++ get selectedMechanism() { ++ return this._selectedMechanism; ++ } ++ ++ get enabledMechanisms() { ++ return this._enabledMechanisms; ++ } ++ ++ get _roleToService() { ++ return this.constructor.RoleToService; ++ } ++ ++ get supportedRoles() { ++ return this.constructor.SupportedRoles; ++ } ++ ++ selectChoice(serviceName, key) { ++ this._handleSelectChoice(serviceName, key); ++ } ++ ++ answerQuery(serviceName, answer) { ++ this._handleAnswerQuery(serviceName, answer); ++ } ++ ++ async beginVerification(userName, userVerifierProxies) { ++ this._cancellable?.cancel(); ++ this._cancellable = new Gio.Cancellable(); ++ this._userName = userName; ++ ++ try { ++ this._updateUserVerifier(userVerifierProxies); ++ await this._startServices(this._cancellable); ++ } catch (e) { ++ if (e.error?.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) ++ return; ++ ++ this._failCounter++; ++ throw e; ++ } ++ ++ this._handleBeginVerification(); ++ } ++ ++ selectMechanism(mechanism) { ++ if (this._selectedMechanism?.role === mechanism.role && ++ this._selectedMechanism?.serviceName === mechanism.serviceName) ++ return false; ++ ++ this._selectedMechanism = this._enabledMechanisms?.find(m => ++ m.role === mechanism.role && ++ m.serviceName === mechanism.serviceName ++ ); ++ ++ this._handleSelectMechanism(); ++ ++ return !!this._selectedMechanism; ++ } ++ ++ needsUsername() { ++ return this._handleNeedsUsername(); ++ } ++ ++ reset() { ++ this._failCounter = 0; ++ ++ this._handleReset(); ++ } ++ ++ cancel() { ++ this._handleCancel(); ++ } ++ ++ destroy() { ++ this.reset(); ++ this.clear(); ++ this.emit('destroy'); ++ } ++ ++ clear() { ++ this._cancellable?.cancel(); ++ this._cancellable = null; ++ ++ this._unavailableServices.clear(); ++ this._activeServices.clear(); ++ ++ this._verificationComplete = false; ++ ++ this._clearUserVerifier(); ++ ++ this._handleClear(); ++ } ++ ++ _clearUserVerifier() { ++ this._disconnectUserVerifierSignals(); ++ this._userVerifier = null; ++ this._userVerifierChoiceList = null; ++ this._userVerifierCustomJSON = null; ++ } ++ ++ _disconnectUserVerifierSignals() { ++ this._userVerifier?.get_connection().disconnectObject(this); ++ this._userVerifier?.disconnectObject(this); ++ this._userVerifierChoiceList?.disconnectObject(this); ++ this._userVerifierCustomJSON?.disconnectObject(this); ++ } ++ ++ _updateEnabledMechanisms() { ++ this._enabledMechanisms = []; ++ ++ this._handleUpdateEnabledMechanisms(); ++ ++ this.emit('mechanisms-changed'); ++ } ++ ++ _connectSmartcardManager() { ++ this._smartcardManager = SmartcardManager.getSmartcardManager(); ++ this._smartcardManager.connectObject( ++ 'smartcard-inserted', () => this._handleSmartcardChanged(), ++ 'smartcard-removed', () => this._handleSmartcardChanged(), ++ this); ++ } ++ ++ _connectFingerprintManager() { ++ // Fingerprint can only work on lockscreen ++ if (!this._reauthOnly) ++ return; ++ ++ this._fingerprintManager = FingerprintManager.getFingerprintManager(); ++ this._fingerprintManager.connectObject( ++ 'reader-type-changed', () => this._handleFingerprintChanged(), ++ this); ++ } ++ ++ _waitPendingMessages() { ++ const cancellable = this._cancellable; ++ const timeoutId = GLib.timeout_add_seconds_once(GLib.PRIORITY_DEFAULT, 10, ++ () => cancellable.cancel()); ++ ++ const {promise, resolve, reject} = Promise.withResolvers(); ++ const task = Gio.Task.new(this, cancellable, () => { ++ try { ++ const res = task.propagate_boolean(); ++ if (!res) ++ throw new GLib.Error(Gio.IOErrorEnum, Gio.IOErrorEnum.FAILED, 'Operation failed'); ++ resolve(); ++ } catch (e) { ++ reject(e); ++ } finally { ++ GLib.source_remove(timeoutId); ++ } ++ }); ++ ++ this.emit('wait-pending-messages', task); ++ ++ return promise; ++ } ++ ++ _updateUserVerifier(proxies) { ++ this._disconnectUserVerifierSignals(); ++ this._userVerifier = proxies.userVerifier; ++ this._userVerifierChoiceList = proxies.userVerifierChoiceList; ++ this._userVerifierCustomJSON = proxies.userVerifierCustomJSON; ++ this._connectUserVerifierSignals(); ++ } ++ ++ _connectUserVerifierSignals() { ++ this._userVerifier.get_connection().connectObject( ++ 'closed', () => this._clearUserVerifier(), ++ this); ++ ++ this._userVerifier.connectObject( ++ 'info', (_, ...args) => this._onInfo(...args), ++ 'problem', (_, ...args) => this._onProblem(...args), ++ 'info-query', (_, ...args) => this._onInfoQuery(...args), ++ 'secret-info-query', (_, ...args) => this._onSecretInfoQuery(...args), ++ 'conversation-started', (_, ...args) => this._onConversationStarted(...args), ++ 'conversation-stopped', (_, ...args) => this._onConversationStopped(...args), ++ 'service-unavailable', (_, ...args) => this._onServiceUnavailable(...args), ++ 'reset', () => this.emit('reset', {}), ++ 'verification-complete', (_, ...args) => this._onVerificationComplete(...args), ++ this); ++ ++ this._userVerifierChoiceList?.connectObject( ++ 'choice-query', (_, ...args) => this._onChoiceListQuery(...args), ++ this); ++ ++ this._userVerifierCustomJSON?.connectObject( ++ 'request', (_, ...args) => this._onCustomJSONRequest(...args), ++ this); ++ } ++ ++ _onInfo(serviceName, info) { ++ this._handleOnInfo(serviceName, info); ++ } ++ ++ _onProblem(serviceName, problem) { ++ this._handleOnProblem(serviceName, problem); ++ } ++ ++ _onInfoQuery(serviceName, question) { ++ this._handleOnInfoQuery(serviceName, question); ++ } ++ ++ _onSecretInfoQuery(serviceName, secretQuestion) { ++ this._handleOnSecretInfoQuery(serviceName, secretQuestion); ++ } ++ ++ _onConversationStarted(serviceName) { ++ this._activeServices.add(serviceName); ++ ++ this._handleOnConversationStarted(serviceName); ++ } ++ ++ _onConversationStopped(serviceName) { ++ this._activeServices.delete(serviceName); ++ ++ this.emit('filter-messages', serviceName, Util.MessageType.ERROR); ++ ++ this._handleOnConversationStopped(serviceName); ++ } ++ ++ _onServiceUnavailable(serviceName, errorMessage) { ++ this._unavailableServices.add(serviceName); ++ ++ if (this._selectedMechanism?.serviceName === serviceName && errorMessage) { ++ this.emit('queue-message', ++ serviceName, ++ errorMessage, ++ Util.MessageType.ERROR); ++ } ++ ++ this._handleOnServiceUnavailable(serviceName, errorMessage); ++ } ++ ++ _onVerificationComplete(serviceName) { ++ this._handleOnVerificationComplete(serviceName); ++ this.emit('verification-complete'); ++ } ++ ++ _onChoiceListQuery(serviceName, promptMessage, list) { ++ this._handleOnChoiceListQuery(serviceName, promptMessage, list); ++ } ++ ++ _onCustomJSONRequest(serviceName, protocol, version, json) { ++ this._handleOnCustomJSONRequest(serviceName, protocol, version, json); ++ } ++ ++ _canRetry() { ++ return this._userName && ++ (this._reauthOnly || this._failCounter < this._allowedFailures); ++ } ++ ++ async _verificationFailed(serviceName, shouldRetry) { ++ this._handleVerificationFailed(serviceName); ++ ++ const doneTrying = !shouldRetry || !this._canRetry(); ++ ++ this.emit('verification-failed', serviceName, !doneTrying); ++ ++ try { ++ await this._waitPendingMessages(); ++ this.emit('reset', {softReset: !doneTrying}); ++ } catch (e) { ++ logErrorUnlessCancelled(e); ++ } ++ } ++ ++ 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, cancellable); ++ } ++ } ++ } ++ ++ _getEnabledServices() { ++ const services = this._enabledRoles ++ .map(r => this._roleToService[r]) ++ .filter(s => s); // filter undefined ++ ++ services.push(...this._getCredentialManagerServices()); ++ ++ // Remove duplicates ++ return [...new Set(services)]; ++ } ++ ++ _getCredentialManagerServices() { ++ return this._handleGetCredentialManagerServices(); ++ } ++ ++ _canStartService(serviceName) { ++ return !this._activeServices.has(serviceName) && ++ !this._unavailableServices.has(serviceName) && ++ this._handleCanStartService(serviceName); ++ } ++ ++ async _startService(serviceName, cancellable) { ++ try { ++ this._activeServices.add(serviceName); ++ if (this._userName) { ++ await this._userVerifier.call_begin_verification_for_user( ++ serviceName, this._userName, cancellable); ++ } else { ++ await this._userVerifier.call_begin_verification( ++ serviceName, cancellable); ++ } ++ } catch (e) { ++ this._activeServices.delete(serviceName); ++ if (e instanceof GLib.Error && ++ Gio.DBusError.is_remote_error(e) && ++ Gio.DBusError.get_remote_error(e) === ++ 'org.gnome.DisplayManager.SessionWorker.Error.ServiceUnavailable') ++ this._unavailableServices.add(serviceName); ++ ++ throw new Util.InitError(e, ++ this._userName ++ ? `Failed to start ${serviceName} verification for user` ++ : `Failed to start ${serviceName} verification`, ++ serviceName); ++ } ++ } ++ ++ _handleSelectChoice() {} ++ ++ _handleAnswerQuery() {} ++ ++ _handleBeginVerification() {} ++ ++ _handleSelectMechanism() {} ++ ++ _handleNeedsUsername() { ++ return true; ++ } ++ ++ _handleReset() {} ++ ++ _handleCancel() {} ++ ++ _handleClear() {} ++ ++ _handleUpdateEnabledMechanisms() { ++ throw new GObject.NotImplementedError( ++ `_handleUpdateEnabledMechanisms in ${this.constructor.name}`); ++ } ++ ++ _handleSmartcardChanged() {} ++ ++ _handleFingerprintChanged() {} ++ ++ _handleOnInfo() {} ++ ++ _handleOnProblem() {} ++ ++ _handleOnInfoQuery() {} ++ ++ _handleOnSecretInfoQuery() {} ++ ++ _handleOnConversationStarted() {} ++ ++ _handleOnConversationStopped() {} ++ ++ _handleOnServiceUnavailable() {} ++ ++ _handleOnVerificationComplete() {} ++ ++ _handleOnChoiceListQuery() {} ++ ++ _handleOnCustomJSONRequest() {} ++ ++ _handleVerificationFailed() {} ++ ++ _handleGetCredentialManagerServices() { ++ return []; ++ } ++ ++ _handleCanStartService() { ++ 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..e27107c85e +--- /dev/null ++++ b/js/gdm/authServicesLegacy.js +@@ -0,0 +1,334 @@ ++import GLib from 'gi://GLib'; ++import GObject from 'gi://GObject'; ++ ++import * as Constants from './constants.js'; ++import {FingerprintReaderType} from '../misc/fingerprintManager.js'; ++import {logErrorUnlessCancelled} from '../misc/errorUtils.js'; ++import * as OVirt from './oVirt.js'; ++import * as Util from './util.js'; ++import * as Vmware from './vmware.js'; ++import {AuthServices} from './authServices.js'; ++ ++const FINGERPRINT_ERROR_TIMEOUT_WAIT = 15; ++ ++const Mechanisms = [ ++ { ++ serviceName: Constants.PASSWORD_SERVICE_NAME, ++ role: Constants.PASSWORD_ROLE_NAME, ++ name: _('Password'), ++ }, ++ { ++ serviceName: Constants.SMARTCARD_SERVICE_NAME, ++ role: Constants.SMARTCARD_ROLE_NAME, ++ name: _('Smartcard'), ++ }, ++ { ++ serviceName: Constants.FINGERPRINT_SERVICE_NAME, ++ role: Constants.FINGERPRINT_ROLE_NAME, ++ name: _('Fingerprint'), ++ }, ++]; ++ ++export class AuthServicesLegacy extends AuthServices { ++ static SupportedRoles = [ ++ Constants.PASSWORD_ROLE_NAME, ++ Constants.SMARTCARD_ROLE_NAME, ++ Constants.FINGERPRINT_ROLE_NAME, ++ ]; ++ ++ static RoleToService = { ++ [Constants.PASSWORD_ROLE_NAME]: Constants.PASSWORD_SERVICE_NAME, ++ [Constants.SMARTCARD_ROLE_NAME]: Constants.SMARTCARD_SERVICE_NAME, ++ [Constants.FINGERPRINT_ROLE_NAME]: Constants.FINGERPRINT_SERVICE_NAME, ++ }; ++ ++ static { ++ GObject.registerClass(this); ++ } ++ ++ constructor(params) { ++ super(params); ++ ++ this._updateEnabledMechanisms(); ++ ++ this._credentialManagers = {}; ++ this.addCredentialManager(OVirt.SERVICE_NAME, OVirt.getOVirtCredentialsManager()); ++ this.addCredentialManager(Vmware.SERVICE_NAME, Vmware.getVmwareCredentialsManager()); ++ } ++ ++ _handleSelectChoice(serviceName, key) { ++ if (serviceName !== this._selectedMechanism?.serviceName) ++ return; ++ ++ this._userVerifierChoiceList.call_select_choice( ++ serviceName, key, this._cancellable).catch(logErrorUnlessCancelled); ++ } ++ ++ _handleAnswerQuery(serviceName, answer) { ++ if (serviceName !== this._selectedMechanism?.serviceName) ++ return; ++ ++ if (this._selectedMechanism.role === Constants.SMARTCARD_ROLE_NAME) ++ this._smartcardInProgress = true; ++ ++ this._userVerifier.call_answer_query( ++ serviceName, answer, this._cancellable).catch(logErrorUnlessCancelled); ++ } ++ ++ _handleBeginVerification() { ++ this._fingerprintManager?.checkReaderType(this._cancellable); ++ ++ this.emit('mechanisms-changed'); ++ } ++ ++ _handleSelectMechanism() { ++ if (this._selectedMechanism) ++ this.emit('reset', {softReset: true}); ++ } ++ ++ _handleNeedsUsername() { ++ // Username won't be needed when there's only one mechanism and is ++ // Smartcard, or if the selected mechanism is a credential manager ++ return !(this._enabledMechanisms.length === 1 && ++ this._enabledMechanisms[0].role === Constants.SMARTCARD_ROLE_NAME || ++ Object.keys(this._credentialManagers).includes(this._selectedMechanism?.serviceName)); ++ } ++ ++ _handleReset() { ++ this._selectedMechanism = null; ++ } ++ ++ _handleClear() { ++ this._smartcardInProgress = false; ++ } ++ ++ _handleUpdateEnabledMechanisms() { ++ if (!this._fingerprintManager?.readerFound) { ++ this._enabledMechanisms.push(...Mechanisms.filter(m => ++ this._enabledRoles.includes(m.role) && ++ m.role !== Constants.FINGERPRINT_ROLE_NAME ++ )); ++ } else { ++ this._enabledMechanisms.push(...Mechanisms.filter(m => ++ this._enabledRoles.includes(m.role) ++ )); ++ } ++ } ++ ++ _handleSmartcardChanged() { ++ if (this._selectedMechanism?.role !== Constants.SMARTCARD_ROLE_NAME || ++ this._smartcardInProgress && this._smartcardManager.hasInsertedTokens()) ++ return; ++ ++ this.emit('reset', {softReset: true}); ++ } ++ ++ _handleFingerprintChanged() { ++ if (!this._enabledRoles.includes(Constants.FINGERPRINT_ROLE_NAME)) ++ return; ++ ++ this._updateEnabledMechanisms(); ++ this.emit('reset', {softReset: true, reuseEntryText: true}); ++ } ++ ++ _handleOnInfo(serviceName, info) { ++ if (serviceName === this._selectedMechanism?.serviceName) { ++ this.emit('queue-message', serviceName, info, Util.MessageType.INFO); ++ } else if (serviceName === Constants.FINGERPRINT_SERVICE_NAME && ++ this._enabledMechanisms.some(m => m.serviceName === serviceName)) { ++ // We don't show fingerprint messages directly since it's ++ // not the main auth service. Instead we use the messages ++ // as a cue to display our own message. ++ this.emit('queue-message', ++ serviceName, ++ this._fingerprintManager?.readerType === FingerprintReaderType.SWIPE ++ // Translators: this message is shown below the password entry field ++ // to indicate the user can swipe their finger on the fingerprint reader ++ ? _('(or swipe finger across reader)') ++ // Translators: this message is shown below the password entry field ++ // to indicate the user can place their finger on the fingerprint reader instead ++ : _('(or place finger on reader)'), ++ Util.MessageType.HINT); ++ } ++ } ++ ++ _handleOnProblem(serviceName, problem) { ++ if (serviceName === this._selectedMechanism?.serviceName || ++ (serviceName === Constants.FINGERPRINT_SERVICE_NAME && ++ this._enabledMechanisms.some(m => m.serviceName === serviceName))) { ++ this.emit('queue-priority-message', ++ serviceName, ++ problem, ++ Util.MessageType.ERROR); ++ } ++ ++ if (serviceName === Constants.FINGERPRINT_SERVICE_NAME && ++ this._enabledMechanisms.some(m => m.serviceName === serviceName)) { ++ // pam_fprintd allows the user to retry multiple (maybe even infinite! ++ // times before failing the authentication conversation. ++ // We don't want this behavior to bypass the max-tries setting the user has set, ++ // so we count the problem messages to know how many times the user has failed. ++ // Once we hit the max number of failures we allow, it's time to failure the ++ // conversation from our side. We can't do that right away, however, because ++ // we may drop pending messages coming from pam_fprintd. In order to make sure ++ // the user sees everything, we queue the failure up to get handled in the ++ // near future, after we've finished up the current round of messages. ++ this._failCounter++; ++ ++ if (this._canRetry()) ++ return; ++ ++ if (this._fingerprintFailedId) ++ GLib.source_remove(this._fingerprintFailedId); ++ ++ this._fingerprintFailedId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, ++ FINGERPRINT_ERROR_TIMEOUT_WAIT, () => { ++ this._fingerprintFailedId = 0; ++ if (!this._cancellable.is_cancelled()) ++ this._verificationFailed(serviceName, false); ++ return GLib.SOURCE_REMOVE; ++ }); ++ } ++ } ++ ++ _handleOnInfoQuery(serviceName, question) { ++ if (serviceName !== this._selectedMechanism?.serviceName) ++ return; ++ ++ this.emit('ask-question', serviceName, question, false); ++ } ++ ++ _handleOnSecretInfoQuery(serviceName, secretQuestion) { ++ if (serviceName !== this._selectedMechanism?.serviceName) ++ return; ++ ++ let token = null; ++ if (this._credentialManagers[serviceName]) ++ token = this._credentialManagers[serviceName].token; ++ ++ if (token) { ++ this.answerQuery(serviceName, token); ++ return; ++ } ++ ++ this.emit('ask-question', serviceName, secretQuestion, true); ++ } ++ ++ _handleOnConversationStopped(serviceName) { ++ if (serviceName !== this._selectedMechanism?.serviceName && ++ serviceName !== Constants.FINGERPRINT_SERVICE_NAME) ++ return; ++ ++ // If the login failed with the preauthenticated oVirt credentials ++ // then discard the credentials and revert to default authentication ++ // mechanism. ++ if (this._credentialManagers[serviceName]) { ++ this._credentialManagers[serviceName].token = null; ++ this._selectedMechanism = null; ++ this._verificationFailed(serviceName, false); ++ return; ++ } ++ ++ if (serviceName === Constants.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 ++ if (serviceName === this._selectedMechanism?.serviceName) ++ this._failCounter++; ++ ++ this._verificationFailed(serviceName, true); ++ } ++ ++ _handleOnServiceUnavailable(serviceName, errorMessage) { ++ if (serviceName !== Constants.FINGERPRINT_SERVICE_NAME || ++ !this._enabledMechanisms.some(m => m.serviceName === serviceName) || ++ !errorMessage) ++ return; ++ ++ this.emit('queue-message', ++ serviceName, ++ errorMessage, ++ Util.MessageType.ERROR); ++ } ++ ++ _handleVerificationFailed(serviceName) { ++ if (serviceName === Constants.FINGERPRINT_SERVICE_NAME && ++ this._enabledMechanisms.some(m => m.serviceName === serviceName) && ++ this._fingerprintFailedId) ++ GLib.source_remove(this._fingerprintFailedId); ++ } ++ ++ _handleOnVerificationComplete(serviceName) { ++ if (serviceName !== this._selectedMechanism?.serviceName) ++ return; ++ ++ if (this._credentialManagers[serviceName]) { ++ this._credentialManagers[serviceName].token = null; ++ this._selectedMechanism = null; ++ } ++ } ++ ++ _handleOnChoiceListQuery(serviceName, promptMessage, list) { ++ if (serviceName !== this._selectedMechanism?.serviceName) ++ return; ++ ++ const choiceList = {}; ++ for (const [key, value] of Object.entries(list.deepUnpack())) ++ choiceList[key] = {title: value}; ++ ++ this.emit('show-choice-list', serviceName, promptMessage, choiceList); ++ } ++ ++ _handleGetCredentialManagerServices() { ++ return Object.keys(this._credentialManagers); ++ } ++ ++ _handleCanStartService(serviceName) { ++ return serviceName === this._selectedMechanism?.serviceName || ++ (serviceName === Constants.FINGERPRINT_SERVICE_NAME && ++ this._enabledMechanisms.some(m => m.serviceName === serviceName) && ++ this._userName); ++ } ++ ++ addCredentialManager(serviceName, credentialManager) { ++ if (this._credentialManagers[serviceName]) ++ return; ++ ++ this._credentialManagers[serviceName] = credentialManager; ++ if (credentialManager.token) ++ this._onCredentialManagerAuthenticated(credentialManager); ++ ++ credentialManager.connectObject( ++ 'user-authenticated', () => this._onCredentialManagerAuthenticated(credentialManager), ++ this); ++ } ++ ++ removeCredentialManager(serviceName) { ++ const credentialManager = this._credentialManagers[serviceName]; ++ if (!credentialManager) ++ return; ++ ++ credentialManager.disconnectObject(this); ++ delete this._credentialManagers[serviceName]; ++ ++ if (this._selectedMechanism?.serviceName === serviceName) ++ this.emit('reset'); ++ } ++ ++ _onCredentialManagerAuthenticated(credentialManager) { ++ this._selectedMechanism = { ++ serviceName: credentialManager.service, ++ role: Constants.PASSWORD_ROLE_NAME, ++ }; ++ this.emit('reset', {softReset: true}); ++ } ++} +diff --git a/js/gdm/util.js b/js/gdm/util.js +index 1176f77500..2172e7b923 100644 +--- a/js/gdm/util.js ++++ b/js/gdm/util.js +@@ -2,29 +2,14 @@ + /* exported BANNER_MESSAGE_KEY, BANNER_MESSAGE_TEXT_KEY, LOGO_KEY, + DISABLE_USER_LIST_KEY, fadeInActor, fadeOutActor, cloneAndFadeOutActor */ + +-const { Clutter, Gdm, Gio, GLib } = imports.gi; ++const { Clutter, Gio, GLib } = imports.gi; + const Signals = imports.signals; + + const Batch = imports.gdm.batch; + const Constants = imports.gdm.constants; +-const {FingerprintManager, FingerprintReaderType} = imports.misc.fingerprintManager; +-const OVirt = imports.gdm.oVirt; +-const Vmware = imports.gdm.vmware; + const Main = imports.ui.main; +-const { loadInterfaceXML } = imports.misc.fileUtils; + const Params = imports.misc.params; +-const SmartcardManager = imports.misc.smartcardManager; +- +-Gio._promisify(Gdm.Client.prototype, +- 'open_reauthentication_channel', 'open_reauthentication_channel_finish'); +-Gio._promisify(Gdm.Client.prototype, +- 'get_user_verifier', 'get_user_verifier_finish'); +-Gio._promisify(Gdm.UserVerifierProxy.prototype, +- 'call_begin_verification_for_user', 'call_begin_verification_for_user_finish'); +-Gio._promisify(Gdm.UserVerifierProxy.prototype, +- 'call_begin_verification', 'call_begin_verification_finish'); +-Gio._promisify(Gio.DBusProxy.prototype, +- 'call', 'call_finish'); ++const { AuthServicesLegacy } = imports.gdm.authServicesLegacy; + + var FADE_ANIMATION_TIME = 160; + var CLONE_FADE_ANIMATION_TIME = 250; +@@ -47,9 +32,6 @@ const MESSAGE_TIME_MULTIPLIER = (() => { + return Number.isFinite(value) && value > 0 ? value : 1; + })(); + +-const FINGERPRINT_SERVICE_PROXY_TIMEOUT = 5000; +-const FINGERPRINT_ERROR_TIMEOUT_WAIT = 15; +- + var MessageType = { + // Keep messages in order by priority + NONE: 0, +@@ -132,6 +114,20 @@ function cloneAndFadeOutActor(actor) { + return hold; + } + ++/** ++ * Error thrown during the authentication initialization phase. ++ * ++ * This error is emitted when requesting user verifier proxies or starting ++ * a service via beginVerification fails. It wraps the underlying error ++ * and provides context about which service failed. ++ */ ++export class InitError extends Error { ++ constructor(error, message, serviceName) { ++ super(message, {cause: error}); ++ this.serviceName = serviceName; ++ } ++} ++ + /** + * @param {object} mechanism + * @returns {boolean} +@@ -174,41 +170,12 @@ var ShellUserVerifier = class { + this._client = client; + this._cancellable = null; + +- this._defaultService = null; +- this._preemptingService = null; +- this._fingerprintReaderType = FingerprintReaderType.NONE; +- this._fingerprintReaderFound = false; +- + this._messageQueue = []; + this._messageQueueTimeoutId = 0; + +- this._failCounter = 0; +- this._activeServices = new Set(); +- this._unavailableServices = new Set(); +- +- this._credentialManagers = {}; +- +- this.reauthenticating = false; +- this.smartcardDetected = false; +- +- this._settings = new Gio.Settings({ schema_id: LOGIN_SCREEN_SCHEMA }); ++ this._settings = new Gio.Settings({schema_id: LOGIN_SCREEN_SCHEMA}); + this._settings.connect('changed', () => this._onSettingsChanged()); +- this._updateEnabledServices(); +- this._updateDefaultService(); +- +- this._credentialManagers[OVirt.SERVICE_NAME] = OVirt.getOVirtCredentialsManager(); +- this._credentialManagers[Vmware.SERVICE_NAME] = Vmware.getVmwareCredentialsManager(); +- +- for (let service in this._credentialManagers) { +- if (this._credentialManagers[service].token) { +- this._onCredentialManagerAuthenticated(this._credentialManagers[service], +- this._credentialManagers[service].token); +- } +- +- this._credentialManagers[service]._authenticatedSignalId = +- this._credentialManagers[service].connect('user-authenticated', +- this._onCredentialManagerAuthenticated.bind(this)); +- } ++ this._updateAuthServices(); + } + + get hasPendingMessages() { +@@ -223,95 +190,85 @@ var ShellUserVerifier = class { + 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; +- this.reauthenticating = false; + +- this._fingerprintManager?.checkReaderType(this._cancellable); ++ try { ++ const proxies = await this._getUserVerifierProxies(userName, this._cancellable); ++ await this._authServicesLegacy?.beginVerification(userName, proxies); ++ this._userVerifier = proxies.userVerifier; ++ } catch (e) { ++ if (e instanceof InitError) ++ this._reportInitError(e); ++ } + +- // If possible, reauthenticate an already running session, +- // so any session specific credentials get updated appropriately +- if (userName) +- this._openReauthenticationChannel(userName); +- else +- this._getUserVerifier(); ++ hold?.release(); + } + + selectMechanism(mechanism) { +- // TODO: Implement mechanism selection +- return false; ++ return this._authServicesLegacy?.selectMechanism(mechanism); + } + +- cancel() { +- if (this._cancellable) +- this._cancellable.cancel(); ++ needsUsername() { ++ return this._authServicesLegacy?.needsUsername(); ++ } + +- if (this._userVerifier) { +- this._userVerifier.call_cancel_sync(null); +- this.clear(); +- } ++ reset() { ++ this._authServicesLegacy?.reset(); ++ ++ this._userVerifier?.call_cancel_sync(null); ++ ++ this.clear(); + } + +- _clearUserVerifier() { +- if (this._userVerifier) { +- this._disconnectSignals(); +- this._userVerifier.run_dispose(); +- this._userVerifier = null; +- if (this._userVerifierChoiceList) { +- this._userVerifierChoiceList.run_dispose(); +- this._userVerifierChoiceList = null; +- } +- } ++ cancel() { ++ this._authServicesLegacy?.cancel(); ++ ++ this._userVerifier?.call_cancel_sync(null); ++ ++ this.clear(); + } + + clear() { +- if (this._cancellable) { +- this._cancellable.cancel(); +- this._cancellable = null; +- } ++ this._authServicesLegacy?.clear(); + +- this._clearUserVerifier(); + this._clearMessageQueue(); +- this._activeServices.clear(); ++ ++ this._cancellable?.cancel(); ++ this._cancellable = null; ++ ++ this._userVerifier = null; + } + + destroy() { ++ this._authServicesLegacy?.destroy(); ++ this._authServicesLegacy = null; ++ + this.cancel(); + + this._settings.run_dispose(); + this._settings = null; ++ } + +- this._smartcardManager?.disconnect(this._smartcardInsertedId); +- this._smartcardManager?.disconnect(this._smartcardRemovedId); +- this._smartcardManager = null; ++ selectChoice(serviceName, key) { ++ this._authServicesLegacy?.selectChoice(serviceName, key); ++ } + +- this._fingerprintManager?.disconnectObject(this); +- this._fingerprintManager = null; ++ async answerQuery(serviceName, answer) { ++ // Wait for pending messages to be displayed before answering to ++ // ensure no messages get lost ++ await this._handlePendingMessages().catch(logErrorUnlessCancelled); + +- for (let service in this._credentialManagers) { +- let credentialManager = this._credentialManagers[service]; +- credentialManager.disconnect(credentialManager._authenticatedSignalId); +- credentialManager = null; +- } ++ this._authServicesLegacy?.answerQuery(serviceName, answer); + } + +- selectChoice(serviceName, key) { +- this._userVerifierChoiceList.call_select_choice(serviceName, key, this._cancellable, null); ++ addCredentialManager(serviceName, credentialManager) { ++ this._authServicesLegacy?.addCredentialManager(serviceName, credentialManager); + } + +- answerQuery(serviceName, answer) { +- if (!this.hasPendingMessages) { +- this._userVerifier.call_answer_query(serviceName, answer, this._cancellable, null); +- } else { +- const cancellable = this._cancellable; +- let signalId = this.connect('no-more-messages', () => { +- this.disconnect(signalId); +- if (!cancellable.is_cancelled()) +- this._userVerifier.call_answer_query(serviceName, answer, cancellable, null); +- }); +- } ++ removeCredentialManager(serviceName) { ++ this._authServicesLegacy?.removeCredentialManager(serviceName); + } + + _getIntervalForMessage(message) { +@@ -322,7 +279,7 @@ var ShellUserVerifier = class { + return message.length * USER_READ_TIME * MESSAGE_TIME_MULTIPLIER; + } + +- finishMessageQueue() { ++ _finishMessageQueue() { + if (!this.hasPendingMessages) + return; + +@@ -365,7 +322,7 @@ var ShellUserVerifier = class { + this._messageQueue.shift(); + this._queueMessageTimeout(); + } else { +- this.finishMessageQueue(); ++ this._finishMessageQueue(); + } + + return GLib.SOURCE_REMOVE; +@@ -395,7 +352,7 @@ var ShellUserVerifier = class { + } + + _clearMessageQueue() { +- this.finishMessageQueue(); ++ this._finishMessageQueue(); + + if (this._messageQueueTimeoutId != 0) { + GLib.source_remove(this._messageQueueTimeoutId); +@@ -404,459 +361,143 @@ var ShellUserVerifier = class { + this.emit('show-message', null, null, MessageType.NONE); + } + +- async _initFingerprintManager() { +- if (this._fingerprintManager) +- return; +- +- this._fingerprintManager = new FingerprintManager(this._cancellable); +- this._fingerprintManager.connectObject( +- 'reader-type-changed', () => this._onFingerprintReaderTypeChanged(), +- this); +- +- if (!this._getDetectedDefaultService()) { +- // Other authentication methods would have already been detected by +- // now as possibilities if they were available. +- // If we're here it means that FINGERPRINT_AUTHENTICATION_KEY is +- // true and so fingerprint authentication is our last potential +- // option, so go ahead a synchronously look for a fingerprint device +- // during startup or default service update. +- // Do not wait too much for fprintd to reply, as in case it hangs +- // we should fail early without having the shell to misbehave +- this._fingerprintManager.setDefaultTimeout(FINGERPRINT_SERVICE_PROXY_TIMEOUT); +- await this._fingerprintManager.checkReaderType(this._cancellable); +- } else { +- // Ensure fingerprint service starts, but do not wait for it +- this._fingerprintManager.checkReaderType(this._cancellable); +- } +- } ++ _reportInitError(initError) { ++ const {cause, message, serviceName} = initError; + +- _onFingerprintReaderTypeChanged() { +- this._fingerprintReaderType = this._fingerprintManager.readerType; +- this._fingerprintReaderFound = this._fingerprintManager.readerFound; +- this._updateDefaultService(); +- +- if (this._userVerifier && +- !this._activeServices.has(Constants.FINGERPRINT_SERVICE_NAME)) +- this._maybeStartFingerprintVerification(); +- } +- +- _onCredentialManagerAuthenticated(credentialManager, _token) { +- this._preemptingService = credentialManager.service; +- this.emit('credential-manager-authenticated'); +- } +- +- _initSmartcardManager() { +- if (this._smartcardManager) +- return; +- +- this._smartcardManager = SmartcardManager.getSmartcardManager(); +- +- // We check for smartcards right away, since an inserted smartcard +- // at startup should result in immediately initiating authentication. +- // This is different than fingerprint readers, where we only check them +- // after a user has been picked. +- this.smartcardDetected = false; +- this._checkForSmartcard(); +- +- this._smartcardInsertedId = this._smartcardManager.connect('smartcard-inserted', +- this._checkForSmartcard.bind(this)); +- this._smartcardRemovedId = this._smartcardManager.connect('smartcard-removed', +- this._checkForSmartcard.bind(this)); +- } +- +- _checkForSmartcard() { +- let smartcardDetected; +- +- if (!this._settings.get_boolean(SMARTCARD_AUTHENTICATION_KEY)) +- smartcardDetected = false; +- else if (this._reauthOnly) +- smartcardDetected = this._smartcardManager.hasInsertedLoginToken(); +- else +- smartcardDetected = this._smartcardManager.hasInsertedTokens(); +- +- if (smartcardDetected != this.smartcardDetected) { +- this.smartcardDetected = smartcardDetected; +- +- if (this.smartcardDetected) +- this._preemptingService = Constants.SMARTCARD_SERVICE_NAME; +- else if (this._preemptingService === Constants.SMARTCARD_SERVICE_NAME) +- this._preemptingService = null; +- +- this._updateDefaultService(); +- +- this.emit('smartcard-status-changed'); +- } +- } +- +- _reportInitError(where, error, serviceName) { +- logError(error, where); +- this._hold?.release(); ++ logError(cause, message); + + this._queueMessage(serviceName, _('Authentication error'), MessageType.ERROR); +- this._failCounter++; + this._verificationFailed(serviceName, false); + } + +- async _openReauthenticationChannel(userName) { +- try { +- this._clearUserVerifier(); +- this._userVerifier = await this._client.open_reauthentication_channel( +- userName, this._cancellable); +- } catch (e) { +- if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) +- return; +- if (e.matches(Gio.DBusError, Gio.DBusError.ACCESS_DENIED) && +- !this._reauthOnly) { +- // Gdm emits org.freedesktop.DBus.Error.AccessDenied when there +- // is no session to reauthenticate. Fall back to performing +- // verification from this login session +- this._getUserVerifier(); +- return; ++ async _getUserVerifierProxies(userName, cancellable) { ++ const proxies = {}; ++ ++ if (userName) { ++ try { ++ proxies.userVerifier = await this._client.open_reauthentication_channel( ++ userName, cancellable); ++ } catch (e) { ++ if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) ++ throw e; ++ if (e.matches(Gio.DBusError, Gio.DBusError.ACCESS_DENIED) && ++ !this._reauthOnly) { ++ // Gdm emits org.freedesktop.DBus.Error.AccessDenied when there ++ // is no session to reauthenticate. Fall back to performing ++ // verification from this login session ++ return this._getUserVerifierProxies(null, cancellable); ++ } ++ throw new InitError(e, 'Failed to open reauthentication channel'); ++ } ++ } else { ++ try { ++ proxies.userVerifier = await this._client.get_user_verifier( ++ cancellable); ++ } catch (e) { ++ if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) ++ throw e; ++ throw new InitError(e, 'Failed to obtain user verifier'); + } +- +- this._reportInitError('Failed to open reauthentication channel', e); +- return; + } + +- if (this._client.get_user_verifier_choice_list) +- this._userVerifierChoiceList = this._client.get_user_verifier_choice_list(); +- else +- this._userVerifierChoiceList = null; +- +- this.reauthenticating = true; +- this._connectSignals(); +- this._beginVerification(); +- this._hold?.release(); +- } +- +- async _getUserVerifier() { + try { +- this._clearUserVerifier(); +- this._userVerifier = +- await this._client.get_user_verifier(this._cancellable); ++ if (this._client.get_user_verifier_choice_list) ++ proxies.userVerifierChoiceList = await this._client.get_user_verifier_choice_list(); ++ if (this._client.get_user_verifier_custom_json) ++ proxies.userVerifierCustomJSON = await this._client.get_user_verifier_custom_json(); + } catch (e) { +- if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) +- 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 +- this._userVerifierChoiceList = null; +- +- this._connectSignals(); +- this._beginVerification(); +- this._hold?.release(); +- } +- +- _connectSignals() { +- this._disconnectSignals(); +- this._signalIds = []; +- +- let id = this._userVerifier.connect('info', this._onInfo.bind(this)); +- this._signalIds.push(id); +- id = this._userVerifier.connect('problem', this._onProblem.bind(this)); +- this._signalIds.push(id); +- id = this._userVerifier.connect('info-query', this._onInfoQuery.bind(this)); +- this._signalIds.push(id); +- id = this._userVerifier.connect('secret-info-query', this._onSecretInfoQuery.bind(this)); +- this._signalIds.push(id); +- id = this._userVerifier.connect('conversation-started', this._onConversationStarted.bind(this)); +- this._signalIds.push(id); +- id = this._userVerifier.connect('conversation-stopped', this._onConversationStopped.bind(this)); +- this._signalIds.push(id); +- id = this._userVerifier.connect('service-unavailable', this._onServiceUnavailable.bind(this)); +- this._signalIds.push(id); +- id = this._userVerifier.connect('reset', this._onReset.bind(this)); +- this._signalIds.push(id); +- id = this._userVerifier.connect('verification-complete', this._onVerificationComplete.bind(this)); +- this._signalIds.push(id); +- +- if (this._userVerifierChoiceList) +- this._userVerifierChoiceList.connect('choice-query', this._onChoiceListQuery.bind(this)); +- } +- +- _disconnectSignals() { +- if (!this._signalIds || !this._userVerifier) +- return; +- +- this._signalIds.forEach(s => this._userVerifier.disconnect(s)); +- this._signalIds = []; +- } +- +- _getForegroundService() { +- if (this._preemptingService) +- return this._preemptingService; +- +- return this._defaultService; +- } +- +- serviceIsForeground(serviceName) { +- return serviceName == this._getForegroundService(); +- } +- +- serviceIsDefault(serviceName) { +- return serviceName == this._defaultService; ++ return proxies; + } + + serviceIsFingerprint(serviceName) { +- return this._fingerprintReaderFound && +- serviceName === Constants.FINGERPRINT_SERVICE_NAME; ++ return serviceName === Constants.FINGERPRINT_SERVICE_NAME; + } + + _onSettingsChanged() { +- this._updateEnabledServices(); +- this._updateDefaultService(); ++ this._updateAuthServices(); + } + +- _updateEnabledServices() { +- let needsReset = false; ++ _updateAuthServices() { ++ const enabledRoles = []; + +- if (this._settings.get_boolean(FINGERPRINT_AUTHENTICATION_KEY)) { +- this._initFingerprintManager(); +- } else if (this._fingerprintManager) { +- this._fingerprintManager.disconnectObject(this); +- this._fingerprintManager = null; +- this._fingerprintReaderFound = false; +- this._fingerprintReaderType = FingerprintReaderType.NONE; +- +- if (this._activeServices.has(Constants.FINGERPRINT_SERVICE_NAME)) +- needsReset = true; +- } ++ if (this._settings.get_boolean(PASSWORD_AUTHENTICATION_KEY)) ++ enabledRoles.push(Constants.PASSWORD_ROLE_NAME); ++ if (this._settings.get_boolean(SMARTCARD_AUTHENTICATION_KEY)) ++ enabledRoles.push(Constants.SMARTCARD_ROLE_NAME); ++ if (this._settings.get_boolean(FINGERPRINT_AUTHENTICATION_KEY)) ++ enabledRoles.push(Constants.FINGERPRINT_ROLE_NAME); + +- if (this._settings.get_boolean(SMARTCARD_AUTHENTICATION_KEY)) { +- this._initSmartcardManager(); +- } else if (this._smartcardManager) { +- this._smartcardManager.disconnect(this._smartcardInsertedId); +- this._smartcardManager.disconnect(this._smartcardRemovedId); +- this._smartcardManager = null; +- +- if (this._activeServices.has(Constants.SMARTCARD_SERVICE_NAME)) +- needsReset = true; +- } +- +- if (needsReset) +- this._cancelAndReset(); +- } +- +- _getDetectedDefaultService() { +- if (this._smartcardManager?.loggedInWithToken()) +- return Constants.SMARTCARD_SERVICE_NAME; +- else if (this._settings.get_boolean(PASSWORD_AUTHENTICATION_KEY)) +- return Constants.PASSWORD_SERVICE_NAME; +- else if (this._smartcardManager) +- return Constants.SMARTCARD_SERVICE_NAME; +- else if (this._fingerprintReaderFound) +- return Constants.FINGERPRINT_SERVICE_NAME; +- return null; +- } +- +- _updateDefaultService() { +- const oldDefaultService = this._defaultService; +- this._defaultService = this._getDetectedDefaultService(); +- +- if (!this._defaultService) { +- log("no authentication service is enabled, using password authentication"); +- this._defaultService = Constants.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 (this._userName) { +- await this._userVerifier.call_begin_verification_for_user( +- serviceName, this._userName, this._cancellable); +- } else { +- await this._userVerifier.call_begin_verification( +- serviceName, this._cancellable); +- } +- } catch (e) { +- this._activeServices.delete(serviceName); +- if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) +- return; +- if (!this.serviceIsForeground(serviceName)) { +- logError(e, 'Failed to start %s for %s'.format(serviceName, this._userName)); +- this._hold?.release(); +- return; +- } +- this._reportInitError(this._userName +- ? 'Failed to start %s verification for user'.format(serviceName) +- : 'Failed to start %s verification'.format(serviceName), e, +- serviceName); ++ if (JSON.stringify(enabledRoles) === JSON.stringify(this._enabledRoles)) + return; +- } +- this._hold?.release(); +- } +- +- _beginVerification() { +- this._startService(this._getForegroundService()); +- this._maybeStartFingerprintVerification().catch(logError); +- } + +- async _maybeStartFingerprintVerification() { +- if (this._userName && +- this._fingerprintReaderFound && +- !this.serviceIsForeground(Constants.FINGERPRINT_SERVICE_NAME)) +- await this._startService(Constants.FINGERPRINT_SERVICE_NAME); +- } ++ this._enabledRoles = enabledRoles; + +- _onChoiceListQuery(client, serviceName, promptMessage, list) { +- if (!this.serviceIsForeground(serviceName)) +- return; +- +- 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) { +- if (this.serviceIsForeground(serviceName)) { +- this._queueMessage(serviceName, info, MessageType.INFO); +- } else if (this.serviceIsFingerprint(serviceName)) { +- // We don't show fingerprint messages directly since it's +- // not the main auth service. Instead we use the messages +- // as a cue to display our own message. +- if (this._fingerprintReaderType === FingerprintReaderType.SWIPE) { +- // Translators: this message is shown below the password entry field +- // to indicate the user can swipe their finger on the fingerprint reader +- this._queueMessage(serviceName, _('(or swipe finger across reader)'), +- MessageType.HINT); +- } else { +- // Translators: this message is shown below the password entry field +- // to indicate the user can place their finger on the fingerprint reader instead +- this._queueMessage(serviceName, _('(or place finger on reader)'), +- MessageType.HINT); +- } +- } ++ this._createAuthServices(); + } + +- _onProblem(client, serviceName, problem) { +- const isFingerprint = this.serviceIsFingerprint(serviceName); +- +- if (!this.serviceIsForeground(serviceName) && !isFingerprint) +- return; +- +- this._queuePriorityMessage(serviceName, problem, MessageType.ERROR); +- +- if (isFingerprint) { +- // pam_fprintd allows the user to retry multiple (maybe even infinite! +- // times before failing the authentication conversation. +- // We don't want this behavior to bypass the max-tries setting the user has set, +- // so we count the problem messages to know how many times the user has failed. +- // Once we hit the max number of failures we allow, it's time to failure the +- // conversation from our side. We can't do that right away, however, because +- // we may drop pending messages coming from pam_fprintd. In order to make sure +- // the user sees everything, we queue the failure up to get handled in the +- // near future, after we've finished up the current round of messages. +- this._failCounter++; +- +- if (!this._canRetry()) { +- if (this._fingerprintFailedId) +- GLib.source_remove(this._fingerprintFailedId); +- +- const cancellable = this._cancellable; +- this._fingerprintFailedId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, +- FINGERPRINT_ERROR_TIMEOUT_WAIT, () => { +- this._fingerprintFailedId = 0; +- if (!cancellable.is_cancelled()) +- this._verificationFailed(serviceName, false); +- return GLib.SOURCE_REMOVE; +- }); +- } +- } +- } ++ _createAuthServices() { ++ this._clearAuthServices(); + +- _onInfoQuery(client, serviceName, question) { +- if (!this.serviceIsForeground(serviceName)) +- return; ++ const params = { ++ client: this._client, ++ enabledRoles: this._enabledRoles, ++ allowedFailures: this.allowedFailures, ++ reauthOnly: this._reauthOnly, ++ }; ++ if (AuthServicesLegacy.supportsAny(this._enabledRoles)) ++ this._authServicesLegacy = new AuthServicesLegacy(params); + +- this.emit('ask-question', serviceName, question, false); ++ this._connectAuthServices(); + } + +- _onSecretInfoQuery(client, serviceName, secretQuestion) { +- if (!this.serviceIsForeground(serviceName)) +- return; +- +- let token = null; +- if (this._credentialManagers[serviceName]) +- token = this._credentialManagers[serviceName].token; +- +- if (token) { +- this.answerQuery(serviceName, token); +- return; +- } +- +- this.emit('ask-question', serviceName, secretQuestion, true); ++ _clearAuthServices() { ++ this._authServicesLegacy?.destroy(); ++ this._authServicesLegacy = null; + } + +- _onReset() { +- // Clear previous attempts to authenticate +- this._failCounter = 0; +- this._activeServices.clear(); +- this._unavailableServices.clear(); +- this._updateDefaultService(); +- +- this.emit('reset'); ++ _connectAuthServices() { ++ [ ++ this._authServicesLegacy, ++ ].forEach(authServices => { ++ authServices?.connectObject( ++ 'ask-question', (_, ...args) => this.emit('ask-question', ...args), ++ 'queue-message', (_, ...args) => this._queueMessage(...args), ++ 'queue-priority-message', (_, ...args) => this._queuePriorityMessage(...args), ++ 'wait-pending-messages', (_, ...args) => this._waitPendingMessages(...args), ++ 'filter-messages', (_, ...args) => this._filterServiceMessages(...args), ++ 'verification-failed', (_, ...args) => this._verificationFailed(...args), ++ 'verification-complete', (_, ...args) => this.emit('verification-complete', ...args), ++ 'reset', (_, ...args) => this.emit('reset', ...args), ++ 'show-choice-list', (_, ...args) => this.emit('show-choice-list', ...args), ++ 'mechanisms-changed', (_, ...args) => this._onMechanismsChanged(...args), ++ this); ++ }); + } + +- _onVerificationComplete() { +- this.emit('verification-complete'); ++ _verificationFailed(serviceName, canRetry) { ++ this._filterServiceMessages(serviceName, MessageType.ERROR); ++ this.emit('verification-failed', serviceName, canRetry); + } + +- _cancelAndReset() { +- this.cancel(); +- this._onReset(); +- } ++ _onMechanismsChanged() { ++ const mechanisms = this._authServicesLegacy?.enabledMechanisms ?? []; + +- _retry(serviceName) { +- this._connectSignals(); +- this._startService(serviceName); +- } ++ const selectedMechanism = this._authServicesLegacy?.selectedMechanism ?? ++ mechanisms.find(m => isSelectable(m)) ?? ++ {}; + +- _canRetry() { +- return this._userName && +- (this._reauthOnly || this._failCounter < this.allowedFailures); ++ this.emit('mechanisms-changed', mechanisms, selectedMechanism); + } + +- _verificationFailed(serviceName, shouldRetry) { +- if (serviceName === Constants.FINGERPRINT_SERVICE_NAME) { +- if (this._fingerprintFailedId) +- GLib.source_remove(this._fingerprintFailedId); +- } +- +- // For Not Listed / enterprise logins, immediately reset +- // the dialog +- // Otherwise, when in login mode we allow ALLOWED_FAILURES attempts. +- // After that, we go back to the welcome screen. +- this._filterServiceMessages(serviceName, MessageType.ERROR); +- +- const doneTrying = !shouldRetry || !this._canRetry(); +- +- if (doneTrying) { +- this._disconnectSignals(); +- +- // eslint-disable-next-line no-lonely-if +- if (!this.hasPendingMessages) { +- this._cancelAndReset(); +- } else { +- const cancellable = this._cancellable; +- let signalId = this.connect('no-more-messages', () => { +- this.disconnect(signalId); +- if (!cancellable.is_cancelled()) +- this._cancelAndReset(); +- }); +- } ++ async _waitPendingMessages(task) { ++ try { ++ await this._handlePendingMessages(); ++ task.return_boolean(true); ++ } catch (e) { ++ task.return_error(e); + } + + this.emit('verification-failed', serviceName, !doneTrying); +@@ -873,47 +514,20 @@ var ShellUserVerifier = class { + } + } + +- _onServiceUnavailable(_client, serviceName, errorMessage) { +- this._unavailableServices.add(serviceName); +- +- if (!errorMessage) +- return; +- +- if (this.serviceIsForeground(serviceName) || this.serviceIsFingerprint(serviceName)) +- this._queueMessage(serviceName, errorMessage, MessageType.ERROR); +- } +- +- _onConversationStarted(client, serviceName) { +- this._activeServices.add(serviceName); +- } +- +- _onConversationStopped(client, serviceName) { +- this._activeServices.delete(serviceName); +- +- // If the login failed with the preauthenticated oVirt credentials +- // then discard the credentials and revert to default authentication +- // mechanism. +- let foregroundService = Object.keys(this._credentialManagers).find(service => +- this.serviceIsForeground(service)); +- if (foregroundService) { +- this._credentialManagers[foregroundService].token = null; +- this._preemptingService = null; +- this._verificationFailed(serviceName, false); +- return; +- } +- +- this._filterServiceMessages(serviceName, MessageType.ERROR); +- +- 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 +- if (this.serviceIsForeground(serviceName)) +- this._failCounter++; ++ _handlePendingMessages() { ++ if (!this.hasPendingMessages) ++ return Promise.resolve(); + +- this._verificationFailed(serviceName, true); ++ const cancellable = this._cancellable; ++ return new Promise((resolve, reject) => { ++ const signalId = this.connect('no-more-messages', () => { ++ this.disconnect(signalId); ++ if (cancellable.is_cancelled()) ++ reject(new GLib.Error(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED, 'Operation was cancelled')); ++ else ++ resolve(); ++ }); ++ }); + } + }; + Signals.addSignalMethods(ShellUserVerifier.prototype); +diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml +index df6dbf5070..f2fb20c7a4 100644 +--- a/js/js-resources.gresource.xml ++++ b/js/js-resources.gresource.xml +@@ -4,6 +4,8 @@ + gdm/authList.js + gdm/authMenuButton.js + gdm/authPrompt.js ++ gdm/authServices.js ++ gdm/authServicesLegacy.js + gdm/batch.js + gdm/constants.js + gdm/loginDialog.js +diff --git a/po/POTFILES.in b/po/POTFILES.in +index cb279c1ee5..a7a645a41c 100644 +--- a/po/POTFILES.in ++++ b/po/POTFILES.in +@@ -6,6 +6,7 @@ data/org.gnome.shell.gschema.xml.in + data/org.gnome.Shell.PortalHelper.desktop.in.in + js/dbusServices/extensions/ui/extension-prefs-dialog.ui + js/gdm/authPrompt.js ++js/gdm/authServicesLegacy.js + js/gdm/loginDialog.js + js/gdm/util.js + js/misc/systemActions.js +-- +2.51.0 + + +From 86d48c601147b21a79d3f8c792f2ff4cfa320514 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Mon, 16 Feb 2026 12:21:54 +0100 +Subject: [PATCH 41/48] gdm: Add fingerprint ready state to delay showing icon + +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 | 54 ++++++++++++++++++++++++++++++++---- + 2 files changed, 50 insertions(+), 6 deletions(-) + +diff --git a/js/gdm/authServices.js b/js/gdm/authServices.js +index d522ee352e..b3bfb56c6c 100644 +--- a/js/gdm/authServices.js ++++ b/js/gdm/authServices.js +@@ -90,7 +90,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 e27107c85e..c0b9fdedd8 100644 +--- a/js/gdm/authServicesLegacy.js ++++ b/js/gdm/authServicesLegacy.js +@@ -10,6 +10,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 = [ + { +@@ -54,6 +55,8 @@ export class AuthServicesLegacy extends AuthServices { + this._credentialManagers = {}; + this.addCredentialManager(OVirt.SERVICE_NAME, OVirt.getOVirtCredentialsManager()); + this.addCredentialManager(Vmware.SERVICE_NAME, Vmware.getVmwareCredentialsManager()); ++ ++ this._fingerprintReadyTimeoutId = 0; + } + + _handleSelectChoice(serviceName, key) { +@@ -100,6 +103,41 @@ 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 !== Constants.FINGERPRINT_SERVICE_NAME || ++ this._fingerprintReadyTimeoutId !== 0) ++ return; ++ ++ this._fingerprintReadyTimeoutId = GLib.timeout_add_once( ++ GLib.PRIORITY_DEFAULT, ++ FINGERPRINT_READY_TIMEOUT_MS, ++ () => { ++ this._fingerprintReadyTimeoutId = 0; ++ this._setFingerprintReady(true); ++ }); ++ } ++ ++ _setFingerprintReady(ready) { ++ const mechanism = this._enabledMechanisms.find(m => ++ m.role === Constants.FINGERPRINT_ROLE_NAME); ++ ++ if (!mechanism || mechanism.ready === ready) ++ return; ++ ++ mechanism.ready = ready; ++ ++ if (ready) ++ this.emit('mechanisms-changed'); + } + + _handleUpdateEnabledMechanisms() { +@@ -112,6 +150,10 @@ export class AuthServicesLegacy extends AuthServices { + this._enabledMechanisms.push(...Mechanisms.filter(m => + this._enabledRoles.includes(m.role) + )); ++ ++ // Mark fingerprint as not ready until service confirms ++ // it's working for this user ++ this._setFingerprintReady(false); + } + } + +@@ -229,11 +271,13 @@ export class AuthServicesLegacy extends AuthServices { + return; + } + +- if (serviceName === Constants.FINGERPRINT_SERVICE_NAME && +- this._unavailableServices.has(serviceName)) { +- this._enabledMechanisms = this._enabledMechanisms +- .filter(m => m.serviceName !== serviceName); +- this.emit('mechanisms-changed'); ++ if (serviceName === Constants.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.51.0 + + +From 5ea67b48e5dc62cac9b08bc817c89a40e4b22ecd Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Tue, 6 Feb 2024 14:09:34 -0500 +Subject: [PATCH 42/48] gdm: Add authServicesSSSDSwitchable + +This new authService child is used by SSSD to control multiple authentication +mechanisms from a single PAM conversation using 'gdm-switchable-auth' +service. It relies on the new JSON extension with a new 'auth-selection' +protocol provided by pam_sss. + +This initial commit just supports password mechanism. Future commits will add +support for other mechanisms. + +The basic workflow: +1. Begin verification starts the only service 'gdm-switchable-auth'. +2. _userVerifierCustomJson emits 'request' signal with a JSON message. + That message contains the list of available mechanisms and a priority list + 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, 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, the selected mechanism role is used with + a switch to determine how to format the response and is sent using + _userVerifierCustomJson. +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. + +When a mechanism is selected, both authServices try to store it. +Only the one that has it in _enabledMechanisms keeps it; the other gets null. +The stored selected mechanism indicates whether authServicesLegacy or +authServicesSwitchable will handle interactions. +--- + js/gdm/authServices.js | 1 + + js/gdm/authServicesSSSDSwitchable.js | 166 +++++++++++++++++++++++++++ + js/gdm/loginDialog.js | 5 +- + js/gdm/util.js | 42 ++++++- + js/js-resources.gresource.xml | 1 + + js/ui/unlockDialog.js | 1 + + po/POTFILES.in | 1 + + 7 files changed, 210 insertions(+), 7 deletions(-) + create mode 100644 js/gdm/authServicesSSSDSwitchable.js + +diff --git a/js/gdm/authServices.js b/js/gdm/authServices.js +index b3bfb56c6c..3416ce3055 100644 +--- a/js/gdm/authServices.js ++++ b/js/gdm/authServices.js +@@ -17,6 +17,7 @@ Gio._promisify(Gdm.UserVerifierProxy.prototype, 'call_begin_verification_for_use + Gio._promisify(Gdm.UserVerifierProxy.prototype, 'call_begin_verification'); + Gio._promisify(Gdm.UserVerifierProxy.prototype, 'call_answer_query'); + Gio._promisify(Gdm.UserVerifierChoiceListProxy.prototype, 'call_select_choice'); ++Gio._promisify(Gdm.UserVerifierCustomJSONProxy.prototype, 'call_reply'); + + export class AuthServices extends GObject.Object { + static [GObject.signals] = { +diff --git a/js/gdm/authServicesSSSDSwitchable.js b/js/gdm/authServicesSSSDSwitchable.js +new file mode 100644 +index 0000000000..c67933b6f3 +--- /dev/null ++++ b/js/gdm/authServicesSSSDSwitchable.js +@@ -0,0 +1,166 @@ ++import GObject from 'gi://GObject'; ++ ++import * as Constants from './constants.js'; ++import {logErrorUnlessCancelled} from '../misc/errorUtils.js'; ++import * as Util from './util.js'; ++import {AuthServices} from './authServices.js'; ++ ++export class AuthServicesSSSDSwitchable extends AuthServices { ++ static SupportedRoles = [ ++ Constants.PASSWORD_ROLE_NAME, ++ ]; ++ ++ static RoleToService = { ++ [Constants.PASSWORD_ROLE_NAME]: Constants.SWITCHABLE_AUTH_SERVICE_NAME, ++ }; ++ ++ static { ++ GObject.registerClass(this); ++ } ++ ++ constructor(params) { ++ super(params); ++ } ++ ++ _handleAnswerQuery(serviceName, answer) { ++ if (serviceName !== this._selectedMechanism?.serviceName) ++ return; ++ ++ let response; ++ switch (this._selectedMechanism.role) { ++ case Constants.PASSWORD_ROLE_NAME: ++ response = this._formatResponse(answer); ++ this._sendResponse(response); ++ break; ++ } ++ } ++ ++ _handleSelectMechanism() { ++ switch (this._selectedMechanism?.role) { ++ case Constants.PASSWORD_ROLE_NAME: ++ this._startPasswordLogin(); ++ break; ++ } ++ } ++ ++ _handleReset() { ++ this._savedMechanism = null; ++ } ++ ++ _handleCancel() { ++ if (this._selectedMechanism) ++ this._savedMechanism = this._selectedMechanism; ++ } ++ ++ _handleClear() { ++ this._mechanisms = null; ++ this._priorityList = null; ++ this._enabledMechanisms = null; ++ this._selectedMechanism = null; ++ } ++ ++ _handleOnCustomJSONRequest(_serviceName, _protocol, _version, json) { ++ let requestObject; ++ ++ try { ++ requestObject = JSON.parse(json); ++ } catch (e) { ++ logError(e); ++ return; ++ } ++ ++ const {authSelection} = requestObject; ++ if (authSelection) { ++ this._mechanisms = authSelection.mechanisms; ++ this._priorityList = authSelection.priority; ++ ++ if (this._mechanisms) ++ this._updateEnabledMechanisms(); ++ } ++ } ++ ++ _handleUpdateEnabledMechanisms() { ++ this._enabledMechanisms.push(...Object.keys(this._mechanisms) ++ .map(id => ({ ++ serviceName: Constants.SWITCHABLE_AUTH_SERVICE_NAME, ++ id, ++ ...this._mechanisms[id], ++ })) ++ // filter out mechanisms with roles that are not enabled ++ .filter(m => this._enabledRoles.includes(m.role))); ++ ++ const selectedMechanism = ++ this._enabledMechanisms ++ .find(m => this._savedMechanism?.role === m.role) ?? ++ this._priorityList ++ .map(id => this._enabledMechanisms.find(m => m.id === id))[0] ?? ++ this._enabledMechanisms[0]; ++ this.selectMechanism(selectedMechanism); ++ ++ this._savedMechanism = null; ++ } ++ ++ _handleOnInfo(serviceName, info) { ++ if (serviceName === this._selectedMechanism?.serviceName) ++ this.emit('queue-message', serviceName, info, Util.MessageType.INFO); ++ } ++ ++ _handleOnProblem(serviceName, problem) { ++ if (serviceName === this._selectedMechanism?.serviceName) { ++ this.emit('queue-priority-message', ++ serviceName, ++ problem, ++ Util.MessageType.ERROR); ++ } ++ } ++ ++ _handleOnConversationStopped(serviceName) { ++ if (serviceName !== this._selectedMechanism?.serviceName) ++ return; ++ ++ if (this._unavailableServices.has(serviceName)) ++ return; ++ ++ this._failCounter++; ++ this._verificationFailed(serviceName, true); ++ } ++ ++ _handleCanStartService(serviceName) { ++ return serviceName === Constants.SWITCHABLE_AUTH_SERVICE_NAME && ++ !this._enabledMechanisms; ++ } ++ ++ _formatResponse(answer) { ++ const {role, id} = this._selectedMechanism; ++ ++ let response; ++ switch (role) { ++ case Constants.PASSWORD_ROLE_NAME: { ++ response = {password: answer}; ++ break; ++ } ++ default: ++ throw new GObject.NotImplementedError(`formatResponse: ${role}`); ++ } ++ ++ return { ++ authSelection: { ++ status: 'Ok', ++ [id]: response, ++ }, ++ }; ++ } ++ ++ _sendResponse(response) { ++ const {serviceName} = this._selectedMechanism; ++ ++ this._userVerifierCustomJSON.call_reply( ++ serviceName, JSON.stringify(response), this._cancellable).catch(logErrorUnlessCancelled); ++ } ++ ++ _startPasswordLogin() { ++ const {serviceName, prompt} = this._selectedMechanism; ++ ++ this.emit('ask-question', serviceName, prompt, true); ++ } ++} +diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js +index ab085fae49..8e71e4f0ed 100644 +--- a/js/gdm/loginDialog.js ++++ b/js/gdm/loginDialog.js +@@ -326,7 +326,10 @@ var LoginDialog = GObject.registerClass({ + this._gdmClient = new Gdm.Client(); + + try { +- this._gdmClient.set_enabled_extensions([Gdm.UserVerifierChoiceList.interface_info().name]); ++ this._gdmClient.set_enabled_extensions([ ++ Gdm.UserVerifierChoiceList.interface_info().name, ++ Gdm.UserVerifierCustomJSON.interface_info().name, ++ ]); + } catch (e) { + } + +diff --git a/js/gdm/util.js b/js/gdm/util.js +index 2172e7b923..9d9722187d 100644 +--- a/js/gdm/util.js ++++ b/js/gdm/util.js +@@ -10,6 +10,7 @@ const Constants = imports.gdm.constants; + const Main = imports.ui.main; + const Params = imports.misc.params; + const { AuthServicesLegacy } = imports.gdm.authServicesLegacy; ++const { AuthServicesSSSDSwitchable } = imports.gdm.authServicesSSSDSwitchable; + + var FADE_ANIMATION_TIME = 160; + var CLONE_FADE_ANIMATION_TIME = 250; +@@ -18,6 +19,7 @@ var LOGIN_SCREEN_SCHEMA = 'org.gnome.login-screen'; + var PASSWORD_AUTHENTICATION_KEY = 'enable-password-authentication'; + var FINGERPRINT_AUTHENTICATION_KEY = 'enable-fingerprint-authentication'; + var SMARTCARD_AUTHENTICATION_KEY = 'enable-smartcard-authentication'; ++var SWITCHABLE_AUTHENTICATION_KEY = 'enable-switchable-authentication'; + var BANNER_MESSAGE_KEY = 'banner-message-enable'; + var BANNER_MESSAGE_TEXT_KEY = 'banner-message-text'; + var ALLOWED_FAILURES_KEY = 'allowed-failures'; +@@ -196,6 +198,7 @@ var ShellUserVerifier = class { + + try { + 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) { +@@ -207,14 +210,19 @@ var ShellUserVerifier = class { + } + + selectMechanism(mechanism) { +- return 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._authServicesSSSDSwitchable?.needsUsername() || ++ this._authServicesLegacy?.needsUsername(); + } + + reset() { ++ this._authServicesSSSDSwitchable?.reset(); + this._authServicesLegacy?.reset(); + + this._userVerifier?.call_cancel_sync(null); +@@ -223,6 +231,7 @@ var ShellUserVerifier = class { + } + + cancel() { ++ this._authServicesSSSDSwitchable?.cancel(); + this._authServicesLegacy?.cancel(); + + this._userVerifier?.call_cancel_sync(null); +@@ -231,6 +240,7 @@ var ShellUserVerifier = class { + } + + clear() { ++ this._authServicesSSSDSwitchable?.clear(); + this._authServicesLegacy?.clear(); + + this._clearMessageQueue(); +@@ -242,6 +252,9 @@ var ShellUserVerifier = class { + } + + destroy() { ++ this._authServicesSSSDSwitchable?.destroy(); ++ this._authServicesSSSDSwitchable = null; ++ + this._authServicesLegacy?.destroy(); + this._authServicesLegacy = null; + +@@ -252,6 +265,7 @@ var ShellUserVerifier = class { + } + + selectChoice(serviceName, key) { ++ this._authServicesSSSDSwitchable?.selectChoice(serviceName, key); + this._authServicesLegacy?.selectChoice(serviceName, key); + } + +@@ -260,6 +274,7 @@ var ShellUserVerifier = class { + // ensure no messages get lost + await this._handlePendingMessages().catch(logErrorUnlessCancelled); + ++ this._authServicesSSSDSwitchable?.answerQuery(serviceName, answer); + this._authServicesLegacy?.answerQuery(serviceName, answer); + } + +@@ -430,10 +445,15 @@ var ShellUserVerifier = class { + if (this._settings.get_boolean(FINGERPRINT_AUTHENTICATION_KEY)) + enabledRoles.push(Constants.FINGERPRINT_ROLE_NAME); + +- if (JSON.stringify(enabledRoles) === JSON.stringify(this._enabledRoles)) ++ const switchableAuthentication = ++ this._settings.get_boolean(SWITCHABLE_AUTHENTICATION_KEY); ++ ++ if (JSON.stringify(enabledRoles) === JSON.stringify(this._enabledRoles) && ++ switchableAuthentication === this._switchableAuthenticationEnabled) + return; + + this._enabledRoles = enabledRoles; ++ this._switchableAuthenticationEnabled = switchableAuthentication; + + this._createAuthServices(); + } +@@ -447,19 +467,25 @@ var ShellUserVerifier = class { + allowedFailures: this.allowedFailures, + reauthOnly: this._reauthOnly, + }; +- if (AuthServicesLegacy.supportsAny(this._enabledRoles)) ++ if (this._switchableAuthenticationEnabled && ++ AuthServicesSSSDSwitchable.supportsAny(this._enabledRoles)) ++ this._authServicesSSSDSwitchable = new AuthServicesSSSDSwitchable(params); ++ else if (AuthServicesLegacy.supportsAny(this._enabledRoles)) + this._authServicesLegacy = new AuthServicesLegacy(params); + + this._connectAuthServices(); + } + + _clearAuthServices() { ++ this._authServicesSSSDSwitchable?.destroy(); ++ this._authServicesSSSDSwitchable = null; + this._authServicesLegacy?.destroy(); + this._authServicesLegacy = null; + } + + _connectAuthServices() { + [ ++ this._authServicesSSSDSwitchable, + this._authServicesLegacy, + ].forEach(authServices => { + authServices?.connectObject( +@@ -483,9 +509,13 @@ var ShellUserVerifier = class { + } + + _onMechanismsChanged() { +- const mechanisms = this._authServicesLegacy?.enabledMechanisms ?? []; ++ const mechanismsSwitchable = this._authServicesSSSDSwitchable?.enabledMechanisms ?? []; ++ const mechanismsLegacy = this._authServicesLegacy?.enabledMechanisms ?? []; ++ const mechanisms = [...mechanismsSwitchable, ...mechanismsLegacy]; + +- const selectedMechanism = this._authServicesLegacy?.selectedMechanism ?? ++ const 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 f2fb20c7a4..bde0cf66c4 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/authServicesSSSDSwitchable.js + gdm/batch.js + gdm/constants.js + gdm/loginDialog.js +diff --git a/js/ui/unlockDialog.js b/js/ui/unlockDialog.js +index db6f1d4ba1..e4c63c92cb 100644 +--- a/js/ui/unlockDialog.js ++++ b/js/ui/unlockDialog.js +@@ -542,6 +542,7 @@ var UnlockDialog = GObject.registerClass({ + try { + this._gdmClient.set_enabled_extensions([ + Gdm.UserVerifierChoiceList.interface_info().name, ++ Gdm.UserVerifierCustomJSON.interface_info().name, + ]); + } catch (e) { + } +diff --git a/po/POTFILES.in b/po/POTFILES.in +index a7a645a41c..f2592f04c6 100644 +--- a/po/POTFILES.in ++++ b/po/POTFILES.in +@@ -7,6 +7,7 @@ data/org.gnome.Shell.PortalHelper.desktop.in.in + js/dbusServices/extensions/ui/extension-prefs-dialog.ui + js/gdm/authPrompt.js + js/gdm/authServicesLegacy.js ++js/gdm/authServicesSSSDSwitchable.js + js/gdm/loginDialog.js + js/gdm/util.js + js/misc/systemActions.js +-- +2.51.0 + + +From e3989e74e46b5ebddcb2b7dfbbbc3d50888cedab Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Wed, 12 Nov 2025 17:25:33 +0100 +Subject: [PATCH 43/48] authServicesSSSDSwitchable: Allow resetting expired + password + +JSON protocol can't inform when a password is expired, so it's needed to +check the info message. + +When the password is expired, a resetting process will be started. It will +have multiple requests to insert the current password and insert the new +password. This has to be done using the old flow because the PAM JSON protocol +doesn't support this process yet. +--- + js/gdm/authServicesSSSDSwitchable.js | 26 +++++++++++++++++++++++++- + 1 file changed, 25 insertions(+), 1 deletion(-) + +diff --git a/js/gdm/authServicesSSSDSwitchable.js b/js/gdm/authServicesSSSDSwitchable.js +index c67933b6f3..ecbd80dc81 100644 +--- a/js/gdm/authServicesSSSDSwitchable.js ++++ b/js/gdm/authServicesSSSDSwitchable.js +@@ -22,10 +22,18 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + super(params); + } + +- _handleAnswerQuery(serviceName, answer) { ++ async _handleAnswerQuery(serviceName, answer) { + if (serviceName !== this._selectedMechanism?.serviceName) + return; + ++ if (this._selectedMechanism.role === Constants.PASSWORD_ROLE_NAME && ++ this._resettingPassword) { ++ this._userVerifier.call_answer_query(serviceName, ++ answer, ++ this._cancellable).catch(logErrorUnlessCancelled); ++ return; ++ } ++ + let response; + switch (this._selectedMechanism.role) { + case Constants.PASSWORD_ROLE_NAME: +@@ -57,6 +65,8 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + this._priorityList = null; + this._enabledMechanisms = null; + this._selectedMechanism = null; ++ ++ this._resettingPassword = false; + } + + _handleOnCustomJSONRequest(_serviceName, _protocol, _version, json) { +@@ -101,6 +111,13 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + } + + _handleOnInfo(serviceName, info) { ++ // sssd can't inform about expired password from JSON so it's needed ++ // to check the info message and handle the reset using the old flow ++ if (serviceName === this._selectedMechanism?.serviceName && ++ this._selectedMechanism.role === Constants.PASSWORD_ROLE_NAME && ++ info.includes('Password expired. Change your password now')) ++ this._resettingPassword = true; ++ + if (serviceName === this._selectedMechanism?.serviceName) + this.emit('queue-message', serviceName, info, Util.MessageType.INFO); + } +@@ -114,6 +131,13 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + } + } + ++ _handleOnSecretInfoQuery(serviceName, secretQuestion) { ++ if (serviceName === this._selectedMechanism?.serviceName && ++ this._selectedMechanism.role === Constants.PASSWORD_ROLE_NAME && ++ this._resettingPassword) ++ this.emit('ask-question', serviceName, secretQuestion, true); ++ } ++ + _handleOnConversationStopped(serviceName) { + if (serviceName !== this._selectedMechanism?.serviceName) + return; +-- +2.51.0 + + +From 6f02e838291e52e0f3bf6a70e3c5d1d0ad208a6e Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Thu, 21 Aug 2025 22:25:02 +0200 +Subject: [PATCH 44/48] gdm: Allow starting authServicesLegacy as fallback +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +There can be cases where authServicesSSSDSwitchable doesn't support some +authentication methods. +In such cases, we should start authServicesLegacy as a fallback for +those unsupported methods. + +authServicesSSSDSwitchable leads when the fallback will happen emitting +'mechanisms-changed'. Then unsupportedRoles, from authServicesSSSDSwitchable will +be used to update the enabledRoles in authServicesLegacy. + +The supportedRoles and unsupportedRoles properties work together: +- supportedRoles: static per class, defines roles the class can handle +- unsupportedRoles: dynamic, roles from enabledRoles that should be delegated + to another auth service as fallback + +For authServicesSSSDSwitchable: +- WAITING/FOUND state: unsupportedRoles returns enabledRoles not in supportedRoles + (e.g., fingerprint). If a role like password isn't in the JSON mechanisms, + that means it's not enabled for this user — authServicesLegacy shouldn't + handle it. +- NOT_FOUND state: unsupportedRoles returns all enabledRoles, since pam_sss + isn't handling this user and authServicesLegacy must take over completely. + +This way authServicesSSSDSwitchable controls which roles authServicesLegacy +handles, enabling coordination between both services. +--- + js/gdm/authServices.js | 22 ++++++++++ + js/gdm/authServicesLegacy.js | 5 +++ + js/gdm/authServicesSSSDSwitchable.js | 65 +++++++++++++++++++++++++++- + js/gdm/util.js | 28 ++++++++++-- + 4 files changed, 114 insertions(+), 6 deletions(-) + +diff --git a/js/gdm/authServices.js b/js/gdm/authServices.js +index 3416ce3055..cfc16b9d7f 100644 +--- a/js/gdm/authServices.js ++++ b/js/gdm/authServices.js +@@ -102,6 +102,10 @@ export class AuthServices extends GObject.Object { + return this.constructor.SupportedRoles; + } + ++ get unsupportedRoles() { ++ return this._handleGetUnsupportedRoles(); ++ } ++ + selectChoice(serviceName, key) { + this._handleSelectChoice(serviceName, key); + } +@@ -178,6 +182,18 @@ export class AuthServices extends GObject.Object { + this._handleClear(); + } + ++ updateEnabledRoles(roles) { ++ if (this._enabledRoles.length === roles.length && ++ this._enabledRoles.every(r => roles.includes(r))) ++ return false; ++ ++ this._enabledRoles = roles; ++ ++ this._handleUpdateEnabledRoles(); ++ ++ return true; ++ } ++ + _clearUserVerifier() { + this._disconnectUserVerifierSignals(); + this._userVerifier = null; +@@ -409,6 +425,10 @@ export class AuthServices extends GObject.Object { + } + } + ++ _handleGetUnsupportedRoles() { ++ return []; ++ } ++ + _handleSelectChoice() {} + + _handleAnswerQuery() {} +@@ -427,6 +447,8 @@ export class AuthServices extends GObject.Object { + + _handleClear() {} + ++ _handleUpdateEnabledRoles() {} ++ + _handleUpdateEnabledMechanisms() { + throw new GObject.NotImplementedError( + `_handleUpdateEnabledMechanisms in ${this.constructor.name}`); +diff --git a/js/gdm/authServicesLegacy.js b/js/gdm/authServicesLegacy.js +index c0b9fdedd8..c1ff1242dc 100644 +--- a/js/gdm/authServicesLegacy.js ++++ b/js/gdm/authServicesLegacy.js +@@ -140,6 +140,11 @@ export class AuthServicesLegacy extends AuthServices { + this.emit('mechanisms-changed'); + } + ++ _handleUpdateEnabledRoles() { ++ this._selectedMechanism = null; ++ this._updateEnabledMechanisms(); ++ } ++ + _handleUpdateEnabledMechanisms() { + if (!this._fingerprintManager?.readerFound) { + this._enabledMechanisms.push(...Mechanisms.filter(m => +diff --git a/js/gdm/authServicesSSSDSwitchable.js b/js/gdm/authServicesSSSDSwitchable.js +index ecbd80dc81..a33ee0df91 100644 +--- a/js/gdm/authServicesSSSDSwitchable.js ++++ b/js/gdm/authServicesSSSDSwitchable.js +@@ -5,6 +5,12 @@ import {logErrorUnlessCancelled} from '../misc/errorUtils.js'; + import * as Util from './util.js'; + import {AuthServices} from './authServices.js'; + ++const MechanismsStatus = { ++ WAITING: 0, ++ NOT_FOUND: 1, ++ FOUND: 2, ++}; ++ + export class AuthServicesSSSDSwitchable extends AuthServices { + static SupportedRoles = [ + Constants.PASSWORD_ROLE_NAME, +@@ -20,6 +26,8 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + + constructor(params) { + super(params); ++ ++ this._mechanismsStatus = MechanismsStatus.WAITING; + } + + async _handleAnswerQuery(serviceName, answer) { +@@ -51,13 +59,30 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + } + } + ++ _handleGetUnsupportedRoles() { ++ // Until we know the mechanisms or wait for them, ++ // consider supportedRoles as supported ++ switch (this._mechanismsStatus) { ++ case MechanismsStatus.WAITING: ++ case MechanismsStatus.FOUND: ++ return this._enabledRoles.filter(r => !this.supportedRoles.includes(r)); ++ case MechanismsStatus.NOT_FOUND: ++ return this._enabledRoles; ++ default: ++ throw new GObject.NotImplementedError(`invalid MechanismStatus: ${this._mechanismsStatus}`); ++ } ++ } ++ + _handleReset() { + this._savedMechanism = null; ++ this._mechanismsStatus = MechanismsStatus.WAITING; + } + + _handleCancel() { +- if (this._selectedMechanism) ++ if (this._selectedMechanism) { + this._savedMechanism = this._selectedMechanism; ++ this._mechanismsStatus = MechanismsStatus.WAITING; ++ } + } + + _handleClear() { +@@ -72,6 +97,9 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + _handleOnCustomJSONRequest(_serviceName, _protocol, _version, json) { + let requestObject; + ++ if (this._mechanismsStatus !== MechanismsStatus.WAITING) ++ log(`Received unexpected JSON message, something might be wrong`); ++ + try { + requestObject = JSON.parse(json); + } catch (e) { +@@ -87,6 +115,10 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + if (this._mechanisms) + this._updateEnabledMechanisms(); + } ++ ++ this._mechanismsStatus = authSelection ++ ? MechanismsStatus.FOUND ++ : MechanismsStatus.NOT_FOUND; + } + + _handleUpdateEnabledMechanisms() { +@@ -111,6 +143,9 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + } + + _handleOnInfo(serviceName, info) { ++ if (!this._eventExpected()) ++ return; ++ + // sssd can't inform about expired password from JSON so it's needed + // to check the info message and handle the reset using the old flow + if (serviceName === this._selectedMechanism?.serviceName && +@@ -123,6 +158,9 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + } + + _handleOnProblem(serviceName, problem) { ++ if (!this._eventExpected()) ++ return; ++ + if (serviceName === this._selectedMechanism?.serviceName) { + this.emit('queue-priority-message', + serviceName, +@@ -131,7 +169,16 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + } + } + ++ _handleOnInfoQuery() { ++ if (!this._eventExpected()) ++ // eslint-disable-next-line no-useless-return ++ return; ++ } ++ + _handleOnSecretInfoQuery(serviceName, secretQuestion) { ++ if (!this._eventExpected()) ++ return; ++ + if (serviceName === this._selectedMechanism?.serviceName && + this._selectedMechanism.role === Constants.PASSWORD_ROLE_NAME && + this._resettingPassword) +@@ -151,7 +198,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + + _handleCanStartService(serviceName) { + return serviceName === Constants.SWITCHABLE_AUTH_SERVICE_NAME && +- !this._enabledMechanisms; ++ this._mechanismsStatus === MechanismsStatus.WAITING; + } + + _formatResponse(answer) { +@@ -182,6 +229,20 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + serviceName, JSON.stringify(response), this._cancellable).catch(logErrorUnlessCancelled); + } + ++ _eventExpected() { ++ // If legacy PAM messages are received before receiving JSON PAM ++ // messages informing about mechanisms, then pam_unix is being ++ // used and the user is not supported by pam_sss using JSON. ++ // Fallback to legacy authentication services. ++ if (this._mechanismsStatus === MechanismsStatus.WAITING) { ++ this._mechanismsStatus = MechanismsStatus.NOT_FOUND; ++ this.emit('mechanisms-changed'); ++ return false; ++ } ++ ++ return true; ++ } ++ + _startPasswordLogin() { + const {serviceName, prompt} = this._selectedMechanism; + +diff --git a/js/gdm/util.js b/js/gdm/util.js +index 9d9722187d..e69761747b 100644 +--- a/js/gdm/util.js ++++ b/js/gdm/util.js +@@ -243,6 +243,11 @@ var ShellUserVerifier = class { + this._authServicesSSSDSwitchable?.clear(); + this._authServicesLegacy?.clear(); + ++ if (this._authServicesSSSDSwitchable) { ++ this._authServicesLegacy?.updateEnabledRoles( ++ this._authServicesSSSDSwitchable.unsupportedRoles); ++ } ++ + this._clearMessageQueue(); + + this._cancellable?.cancel(); +@@ -468,10 +473,14 @@ var ShellUserVerifier = class { + reauthOnly: this._reauthOnly, + }; + if (this._switchableAuthenticationEnabled && +- AuthServicesSSSDSwitchable.supportsAny(this._enabledRoles)) ++ AuthServicesSSSDSwitchable.supportsAny(this._enabledRoles)) { + this._authServicesSSSDSwitchable = new AuthServicesSSSDSwitchable(params); +- else if (AuthServicesLegacy.supportsAny(this._enabledRoles)) ++ ++ params.enabledRoles = this._authServicesSSSDSwitchable.unsupportedRoles; + this._authServicesLegacy = new AuthServicesLegacy(params); ++ } else if (AuthServicesLegacy.supportsAny(this._enabledRoles)) { ++ this._authServicesLegacy = new AuthServicesLegacy(params); ++ } + + this._connectAuthServices(); + } +@@ -509,9 +518,12 @@ var ShellUserVerifier = class { + } + + _onMechanismsChanged() { +- const mechanismsSwitchable = this._authServicesSSSDSwitchable?.enabledMechanisms ?? []; ++ if (this._enableFallbackMechanisms()) ++ return; ++ ++ const mechanismsSSSDSwitchable = this._authServicesSSSDSwitchable?.enabledMechanisms ?? []; + const mechanismsLegacy = this._authServicesLegacy?.enabledMechanisms ?? []; +- const mechanisms = [...mechanismsSwitchable, ...mechanismsLegacy]; ++ const mechanisms = [...mechanismsSSSDSwitchable, ...mechanismsLegacy]; + + const selectedMechanism = + this._authServicesSSSDSwitchable?.selectedMechanism ?? +@@ -522,6 +534,14 @@ var ShellUserVerifier = class { + this.emit('mechanisms-changed', mechanisms, selectedMechanism); + } + ++ _enableFallbackMechanisms() { ++ if (!this._authServicesSSSDSwitchable || !this._authServicesLegacy) ++ return false; ++ ++ return this._authServicesLegacy.updateEnabledRoles( ++ this._authServicesSSSDSwitchable.unsupportedRoles); ++ } ++ + async _waitPendingMessages(task) { + try { + await this._handlePendingMessages(); +-- +2.51.0 + + +From 124b735692aa5f8f892dc739c15c047a2ae64c1d Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= +Date: Wed, 11 Feb 2026 03:36:54 +0100 +Subject: [PATCH 45/48] ui/qrCode: Add a QR Code widget + +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 +--- + .../gnome-shell-sass/widgets/_qr-code.scss | 14 +++ + data/theme/meson.build | 1 + + js/js-resources.gresource.xml | 1 + + js/ui/qrCode.js | 118 ++++++++++++++++++ + 4 files changed, 134 insertions(+) + 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/_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 87112c358d..169262d518 100644 +--- a/data/theme/meson.build ++++ b/data/theme/meson.build +@@ -30,6 +30,7 @@ theme_sources = files([ + 'gnome-shell-sass/widgets/_panel.scss', + 'gnome-shell-sass/widgets/_popovers.scss', + 'gnome-shell-sass/widgets/_screen-shield.scss', ++ 'gnome-shell-sass/widgets/_qr-code.scss', + 'gnome-shell-sass/widgets/_scrollbars.scss', + 'gnome-shell-sass/widgets/_search-entry.scss', + 'gnome-shell-sass/widgets/_search-results.scss', +diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml +index bde0cf66c4..e8575a7b6e 100644 +--- a/js/js-resources.gresource.xml ++++ b/js/js-resources.gresource.xml +@@ -95,6 +95,7 @@ + ui/pointerA11yTimeout.js + ui/pointerWatcher.js + ui/popupMenu.js ++ ui/qrCode.js + ui/remoteSearch.js + ui/ripples.js + ui/runDialog.js +diff --git a/js/ui/qrCode.js b/js/ui/qrCode.js +new file mode 100644 +index 0000000000..caf0999f34 +--- /dev/null ++++ b/js/ui/qrCode.js +@@ -0,0 +1,118 @@ ++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.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.51.0 + + +From 634b290c79fbc7f552b536dbb121e25ea435df41 Mon Sep 17 00:00:00 2001 +From: Ray Strode +Date: Tue, 6 Feb 2024 14:18:24 -0500 +Subject: [PATCH 46/48] gdm: Add support for Web Login in + authServicesSSSDSwitchable +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +SSSD supports Web Login via a url on an external device. It +advertises this support over the 'auth-selection' JSON protocol. + +This commit adds Web Login support by showing the new WebLoginDialog that +shows an url and a QR code. + +Use the bindings of the new GnomeQR library to generate the QR code. + +Co-authored-by: Marco Trevisan (Treviño) +--- + js/gdm/authPrompt.js | 105 ++++++++- + js/gdm/authServices.js | 6 + + js/gdm/authServicesSSSDSwitchable.js | 80 +++++++ + js/gdm/constants.js | 1 + + js/gdm/loginDialog.js | 5 +- + js/gdm/util.js | 5 + + js/gdm/webLogin.js | 308 +++++++++++++++++++++++++++ + js/js-resources.gresource.xml | 1 + + js/ui/unlockDialog.js | 3 +- + js/ui/userWidget.js | 8 + + po/POTFILES.in | 1 + + 11 files changed, 511 insertions(+), 12 deletions(-) + create mode 100644 js/gdm/webLogin.js + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index 5a92c725b5..8c5c8a1fab 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -12,6 +12,7 @@ const Vmware = imports.gdm.vmware; + const Params = imports.misc.params; + const ShellEntry = imports.ui.shellEntry; + const UserWidget = imports.ui.userWidget; ++const WebLogin = imports.gdm.webLogin; + const Util = imports.misc.util; + + var DEFAULT_BUTTON_WELL_ICON_SIZE = 16; +@@ -85,6 +86,7 @@ var AuthPrompt = GObject.registerClass({ + 'show-message', this._onShowMessage.bind(this), + 'show-choice-list', this._onShowChoiceList.bind(this), + 'mechanisms-changed', (_, ...args) => this.emit('mechanisms-changed', ...args), ++ 'web-login', this._onWebLogin.bind(this), + 'verification-failed', this._onVerificationFailed.bind(this), + 'verification-complete', this._onVerificationComplete.bind(this), + 'reset', this._onReset.bind(this), +@@ -297,6 +299,28 @@ var AuthPrompt = GObject.registerClass({ + this._defaultButtonWell.add_child(this._spinner); + + this.setActorInDefaultButtonWell(this._nextButton); ++ ++ this._webLoginIntro = new WebLogin.WebLoginIntro(); ++ this._webLoginIntro.set({ ++ y_expand: true, ++ }); ++ this._webLoginIntro.connect('clicked', () => { ++ this._webLoginIntro.hide(); ++ this._openWebLoginDialog(); ++ }); ++ this._mainBox.add_child(this._webLoginIntro); ++ ++ this._webLoginDialog = new WebLogin.WebLoginDialog(); ++ this._webLoginDialog.connect('cancel', () => { ++ if (this._webLoginDialog.isLoading) { ++ this.reset({softReset: true}); ++ } else { ++ this._closeWebLoginDialog(); ++ this._fadeInElement(this._webLoginIntro); ++ } ++ }); ++ this._webLoginDialog.connect('loading', () => this.emit('loading', this._webLoginDialog.isLoading)); ++ this._inputWell.add_child(this._webLoginDialog); + } + + _updateShowPasswordIcon() { +@@ -440,6 +464,52 @@ var AuthPrompt = GObject.registerClass({ + this.emit('prompted'); + } + ++ _onWebLogin(_userVerifier, serviceName, introMessage, message, url, code, buttons) { ++ if (this._queryingService) ++ this.clear(); ++ ++ this._queryingService = serviceName; ++ this._promptStep++; ++ this._updateCancelButton(); ++ ++ this._webLoginParams = {message, url, code, buttons}; ++ ++ this._entryArea.hide(); ++ ++ if (this._preemptiveAnswer) ++ this._preemptiveAnswer = null; ++ ++ if (!this._webLoginDialog.visible && introMessage) { ++ this._webLoginIntro.setMessage(introMessage); ++ this._fadeInElement(this._webLoginIntro); ++ } else { ++ this._openWebLoginDialog(); ++ } ++ ++ this.emit('prompted'); ++ } ++ ++ _closeWebLoginDialog() { ++ this._webLoginDialog.hide(); ++ ++ this._userWell.get_child()?.showAvatar(); ++ this._mainBox.show(); ++ ++ this.webLoginActive = false; ++ this.remove_style_class_name('web-login-active'); ++ } ++ ++ _openWebLoginDialog() { ++ this._userWell.get_child()?.hideAvatar(); ++ this._mainBox.hide(); ++ ++ this._webLoginDialog.update(this._webLoginParams); ++ this._fadeInElement(this._webLoginDialog); ++ ++ this.webLoginActive = true; ++ this.add_style_class_name('web-login-active'); ++ } ++ + _onShowMessage(_userVerifier, serviceName, message, type) { + this.setMessage(serviceName, message, type); + +@@ -448,7 +518,9 @@ var AuthPrompt = GObject.registerClass({ + if (message && + type < GdmUtil.MessageType.ERROR && + !this._entryArea.visible && +- !this._authList.visible) ++ !this._authList.visible && ++ !this._webLoginIntro.visible && ++ !this._webLoginDialog.visible) + this._fadeInElement(this._entryArea); + + this.emit('prompted'); +@@ -474,12 +546,14 @@ var AuthPrompt = GObject.registerClass({ + this.stopSpinning({animate: true}); + this.verificationStatus = AuthPromptStatus.VERIFICATION_SUCCEEDED; + +- this._mainBox.reactive = false; +- this._mainBox.can_focus = false; +- this._mainBox.ease({ +- opacity: 0, +- duration: MESSAGE_FADE_OUT_ANIMATION_TIME, +- mode: Clutter.AnimationMode.EASE_OUT_QUAD, ++ [this._mainBox, this._webLoginDialog].forEach(widget => { ++ widget.reactive = false; ++ widget.can_focus = false; ++ widget.ease({ ++ opacity: 0, ++ duration: MESSAGE_FADE_OUT_ANIMATION_TIME, ++ mode: Clutter.AnimationMode.EASE_OUT_QUAD, ++ }); + }); + + this.emit('verification-complete'); +@@ -551,6 +625,9 @@ var AuthPrompt = GObject.registerClass({ + stopSpinning({animate = false} = {}) { + this.emit('loading', false); + this.setActorInDefaultButtonWell(this._nextButton, animate); ++ ++ if (this._webLoginDialog.isLoading) ++ this._webLoginDialog.stopLoading(); + } + + clear(params) { +@@ -568,10 +645,14 @@ var AuthPrompt = GObject.registerClass({ + this._authListTitle.child.text = ''; + this._authList.clear(); + this._authList.hide(); ++ this._webLoginIntro.hide(); ++ this._closeWebLoginDialog(); + +- this._mainBox.opacity = 255; +- this._mainBox.reactive = true; +- this._mainBox.can_focus = true; ++ [this._mainBox, this._webLoginDialog].forEach(widget => { ++ widget.opacity = 255; ++ widget.reactive = true; ++ widget.can_focus = true; ++ }); + } + + setQuestion(question) { +@@ -583,6 +664,8 @@ var AuthPrompt = GObject.registerClass({ + this._entry.hint_text = question; + + this._authList.hide(); ++ this._webLoginIntro.hide(); ++ this._closeWebLoginDialog(); + + this._fadeInElement(this._entryArea); + } +@@ -692,6 +775,8 @@ var 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 cfc16b9d7f..a62a3de901 100644 +--- a/js/gdm/authServices.js ++++ b/js/gdm/authServices.js +@@ -48,6 +48,12 @@ export class AuthServices extends GObject.Object { + param_types: [GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_JSOBJECT], + }, + 'mechanisms-changed': {}, ++ 'web-login': { ++ param_types: [ ++ GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_STRING, ++ GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_JSOBJECT, ++ ], ++ }, + }; + + static { +diff --git a/js/gdm/authServicesSSSDSwitchable.js b/js/gdm/authServicesSSSDSwitchable.js +index a33ee0df91..5369334b0a 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 Constants from './constants.js'; +@@ -14,10 +15,12 @@ const MechanismsStatus = { + export class AuthServicesSSSDSwitchable extends AuthServices { + static SupportedRoles = [ + Constants.PASSWORD_ROLE_NAME, ++ Constants.WEB_LOGIN_ROLE_NAME, + ]; + + static RoleToService = { + [Constants.PASSWORD_ROLE_NAME]: Constants.SWITCHABLE_AUTH_SERVICE_NAME, ++ [Constants.WEB_LOGIN_ROLE_NAME]: Constants.SWITCHABLE_AUTH_SERVICE_NAME, + }; + + static { +@@ -56,6 +59,9 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + case Constants.PASSWORD_ROLE_NAME: + this._startPasswordLogin(); + break; ++ case Constants.WEB_LOGIN_ROLE_NAME: ++ this._startWebLogin(); ++ break; + } + } + +@@ -92,6 +98,8 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + this._selectedMechanism = null; + + this._resettingPassword = false; ++ ++ this._clearWebLoginTimeout(); + } + + _handleOnCustomJSONRequest(_serviceName, _protocol, _version, json) { +@@ -131,6 +139,8 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + // filter out mechanisms with roles that are not enabled + .filter(m => this._enabledRoles.includes(m.role))); + ++ this._trackWebLoginTimeout(); ++ + const selectedMechanism = + this._enabledMechanisms + .find(m => this._savedMechanism?.role === m.role) ?? +@@ -142,6 +152,29 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + this._savedMechanism = null; + } + ++ _trackWebLoginTimeout() { ++ this._clearWebLoginTimeout(); ++ ++ const webLoginMechanism = this._enabledMechanisms ++ .find(m => m.role === Constants.WEB_LOGIN_ROLE_NAME); ++ if (!webLoginMechanism) ++ return; ++ ++ const {timeout} = webLoginMechanism; ++ if (!timeout) ++ return; ++ ++ this._webLoginTimeoutId = GLib.timeout_add_seconds_once(GLib.PRIORITY_DEFAULT, ++ timeout, () => { ++ if (this._selectedMechanism?.role !== Constants.WEB_LOGIN_ROLE_NAME) ++ webLoginMechanism.needsRefresh = true; ++ else ++ this.emit('reset', {softReset: true}); ++ ++ this._webLoginTimeoutId = 0; ++ }); ++ } ++ + _handleOnInfo(serviceName, info) { + if (!this._eventExpected()) + return; +@@ -210,6 +243,10 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + response = {password: answer}; + break; + } ++ case Constants.WEB_LOGIN_ROLE_NAME: { ++ response = {}; ++ break; ++ } + default: + throw new GObject.NotImplementedError(`formatResponse: ${role}`); + } +@@ -248,4 +285,47 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + + this.emit('ask-question', serviceName, prompt, true); + } ++ ++ _startWebLogin() { ++ const { ++ serviceName, ++ initPrompt, linkPrompt, ++ uri, code, needsRefresh, ++ } = this._selectedMechanism; ++ ++ if (!linkPrompt || !uri) ++ return; ++ ++ if (needsRefresh) { ++ this.emit('reset', {softReset: true}); ++ return; ++ } ++ ++ const buttons = [{ ++ default: true, ++ needsLoading: true, ++ label: _('Done'), ++ action: () => this._webLoginDone(), ++ }]; ++ ++ this.emit('web-login', serviceName, initPrompt, linkPrompt, uri, code, buttons); ++ } ++ ++ _webLoginDone() { ++ if (this._selectedMechanism?.role !== Constants.WEB_LOGIN_ROLE_NAME) ++ return; ++ ++ const response = this._formatResponse(); ++ this._sendResponse(response); ++ ++ this._clearWebLoginTimeout(); ++ } ++ ++ _clearWebLoginTimeout() { ++ if (!this._webLoginTimeoutId) ++ return; ++ ++ GLib.source_remove(this._webLoginTimeoutId); ++ this._webLoginTimeoutId = 0; ++ } + } +diff --git a/js/gdm/constants.js b/js/gdm/constants.js +index 2f37446c8a..e8ca48625a 100644 +--- a/js/gdm/constants.js ++++ b/js/gdm/constants.js +@@ -3,6 +3,7 @@ + export const PASSWORD_ROLE_NAME = 'password'; + export const SMARTCARD_ROLE_NAME = 'smartcard'; + export const FINGERPRINT_ROLE_NAME = 'fingerprint'; ++export const WEB_LOGIN_ROLE_NAME = 'eidp'; + + export const PASSWORD_SERVICE_NAME = 'gdm-password'; + export const SMARTCARD_SERVICE_NAME = 'gdm-smartcard'; +diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js +index 8e71e4f0ed..f59663401b 100644 +--- a/js/gdm/loginDialog.js ++++ b/js/gdm/loginDialog.js +@@ -611,7 +611,10 @@ var LoginDialog = GObject.registerClass({ + let authPromptAllocation = null; + let authPromptWidth = 0; + if (this._authPrompt.visible) { +- authPromptAllocation = this._getFixedTopActorAllocation(dialogBox, this._authPrompt); ++ if (this._authPrompt.webLoginActive) ++ authPromptAllocation = this._getCenterActorAllocation(dialogBox, this._authPrompt); ++ else ++ authPromptAllocation = this._getFixedTopActorAllocation(dialogBox, this._authPrompt); + authPromptWidth = authPromptAllocation.x2 - authPromptAllocation.x1; + } + +diff --git a/js/gdm/util.js b/js/gdm/util.js +index e69761747b..6c160d323b 100644 +--- a/js/gdm/util.js ++++ b/js/gdm/util.js +@@ -20,6 +20,7 @@ var PASSWORD_AUTHENTICATION_KEY = 'enable-password-authentication'; + var FINGERPRINT_AUTHENTICATION_KEY = 'enable-fingerprint-authentication'; + var SMARTCARD_AUTHENTICATION_KEY = 'enable-smartcard-authentication'; + var SWITCHABLE_AUTHENTICATION_KEY = 'enable-switchable-authentication'; ++var WEB_AUTHENTICATION_KEY = 'enable-web-authentication'; + var BANNER_MESSAGE_KEY = 'banner-message-enable'; + var BANNER_MESSAGE_TEXT_KEY = 'banner-message-text'; + var ALLOWED_FAILURES_KEY = 'allowed-failures'; +@@ -138,6 +139,7 @@ export function isSelectable(mechanism) { + switch (mechanism.role) { + case Constants.PASSWORD_ROLE_NAME: + case Constants.SMARTCARD_ROLE_NAME: ++ case Constants.WEB_LOGIN_ROLE_NAME: + return true; + case Constants.FINGERPRINT_ROLE_NAME: + return false; +@@ -449,6 +451,8 @@ var ShellUserVerifier = class { + enabledRoles.push(Constants.SMARTCARD_ROLE_NAME); + if (this._settings.get_boolean(FINGERPRINT_AUTHENTICATION_KEY)) + enabledRoles.push(Constants.FINGERPRINT_ROLE_NAME); ++ if (this._settings.get_boolean(WEB_AUTHENTICATION_KEY)) ++ enabledRoles.push(Constants.WEB_LOGIN_ROLE_NAME); + + const switchableAuthentication = + this._settings.get_boolean(SWITCHABLE_AUTHENTICATION_KEY); +@@ -508,6 +512,7 @@ var ShellUserVerifier = class { + 'reset', (_, ...args) => this.emit('reset', ...args), + 'show-choice-list', (_, ...args) => this.emit('show-choice-list', ...args), + 'mechanisms-changed', (_, ...args) => this._onMechanismsChanged(...args), ++ 'web-login', (_, ...args) => this.emit('web-login', ...args), + this); + }); + } +diff --git a/js/gdm/webLogin.js b/js/gdm/webLogin.js +new file mode 100644 +index 0000000000..8f2a0d8767 +--- /dev/null ++++ b/js/gdm/webLogin.js +@@ -0,0 +1,308 @@ ++// -*- 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 GObject from 'gi://GObject'; ++import Pango from 'gi://Pango'; ++import St from 'gi://St'; ++import {Spinner} from '../ui/animation.js'; ++import * as Params from '../misc/params.js'; ++import {QrCode} from '../ui/qrCode.js'; ++ ++const QR_CODE_SIZE = 150; ++const WEB_LOGIN_SPINNER_SIZE = 35; ++const URL_LABEL_LONG_THRESHOLD = 45; ++ ++export const WebLoginPrompt = GObject.registerClass( ++class WebLoginPrompt extends St.BoxLayout { ++ constructor(params) { ++ const {qrSize: qrCodeSize, message, url, code} = Params.parse(params, { ++ qrSize: QR_CODE_SIZE, ++ message: null, ++ url: null, ++ code: null, ++ }); ++ ++ super({ ++ styleClass: 'web-login-prompt', ++ vertical: true, ++ y_align: Clutter.ActorAlign.CENTER, ++ }); ++ ++ this._urlTitleLabel = new St.Label({ ++ style_class: 'web-login-title-label', ++ }); ++ this._urlTitleLabel.clutterText.set({ ++ lineWrap: true, ++ ellipsize: Pango.EllipsizeMode.NONE, ++ }); ++ this.add_child(this._urlTitleLabel); ++ ++ this._qrCode = new QrCode({ ++ width: qrCodeSize, ++ height: qrCodeSize, ++ xAlign: Clutter.ActorAlign.CENTER, ++ }); ++ this.add_child(this._qrCode); ++ ++ this._urlLabel = new St.Label({ ++ style_class: 'web-login-url-label', ++ x_expand: true, ++ }); ++ this.add_child(this._urlLabel); ++ ++ this._codeBox = new St.BoxLayout({ ++ x_align: Clutter.ActorAlign.CENTER, ++ x_expand: true, ++ visible: false, ++ }); ++ ++ this._codeTitleLabel = new St.Label({ ++ text: _('Login code: '), ++ style_class: 'web-login-code-title-label', ++ }); ++ this._codeBox.add_child(this._codeTitleLabel); ++ ++ this._codeLabel = new St.Label({ ++ style_class: 'web-login-code-label', ++ }); ++ this._codeBox.add_child(this._codeLabel); ++ this.add_child(this._codeBox); ++ ++ this.update({message, url, code}); ++ } ++ ++ update(params) { ++ const {message, url, code} = Params.parse(params, { ++ message: null, ++ url: null, ++ code: null, ++ }); ++ ++ if (message !== null) ++ this._urlTitleLabel.text = message; ++ ++ if (url !== null) { ++ this._qrCode.set({url}); ++ ++ const formattedUrl = this._formatURLForDisplay(url); ++ this._urlLabel.text = formattedUrl; ++ ++ if (formattedUrl.length > URL_LABEL_LONG_THRESHOLD) ++ this._urlLabel.add_style_class_name('web-login-url-label-long'); ++ else ++ this._urlLabel.remove_style_class_name('web-login-url-label-long'); ++ } ++ ++ if (code !== null) { ++ this._codeLabel.text = code; ++ this._codeBox.show(); ++ } else { ++ this._codeBox.hide(); ++ } ++ } ++ ++ _formatURLForDisplay(url) { ++ const http = 'http://'; ++ const https = 'https://'; ++ ++ if (!url) ++ return http; ++ ++ if (url.endsWith('/')) ++ url = url.substring(0, url.length - 1); ++ ++ if (url.startsWith(http)) ++ return url.substring(http.length); ++ ++ if (url.startsWith(https)) ++ return url.substring(https.length); ++ ++ return url; ++ } ++}); ++ ++export const WebLoginDialog = GObject.registerClass({ ++ Signals: { ++ 'cancel': {}, ++ 'loading': {}, ++ }, ++}, class WebLoginDialog extends St.Widget { ++ constructor(params) { ++ const {message, url, code, buttons} = Params.parse(params, { ++ message: null, ++ url: null, ++ code: null, ++ buttons: [], ++ }); ++ ++ super({ ++ layout_manager: new Clutter.BinLayout(), ++ x_expand: true, ++ y_expand: true, ++ visible: false, ++ }); ++ this.bind_property_full('reactive', ++ this, 'opacity', ++ GObject.BindingFlags.SYNC_CREATE, ++ (_bind, source) => [true, source ? 255 : 128], ++ null); ++ ++ this._contentBox = new St.BoxLayout({ ++ orientation: Clutter.Orientation.VERTICAL, ++ x_expand: true, ++ y_expand: true, ++ }); ++ this.add_child(this._contentBox); ++ ++ this._webLoginPrompt = new WebLoginPrompt({code, message, url}); ++ this._contentBox.add_child(this._webLoginPrompt); ++ ++ this._buttonBox = new St.BoxLayout({ ++ orientation: Clutter.Orientation.HORIZONTAL, ++ x_align: Clutter.ActorAlign.CENTER, ++ x_expand: true, ++ }); ++ this._contentBox.add_child(this._buttonBox); ++ ++ this._updateButtons(buttons); ++ ++ this._spinnerFrame = new St.Widget({ ++ layout_manager: new Clutter.BinLayout(), ++ style_class: 'web-login-spinner', ++ }); ++ this.add_child(this._spinnerFrame); ++ ++ this._spinner = new Spinner(WEB_LOGIN_SPINNER_SIZE, { ++ hideOnStop: true, ++ }); ++ this._spinnerFrame.add_child(this._spinner); ++ } ++ ++ _updateButtons(buttons) { ++ this._buttonBox.destroy_all_children(); ++ this._cancelButton = this._addButton({ ++ label: _('Cancel'), ++ action: () => this.emit('cancel'), ++ }); ++ ++ buttons?.forEach(b => this._addButton(b)); ++ } ++ ++ _addButton(b) { ++ const button = new St.Button({ ++ style_class: 'web-login-prompt-button', ++ can_focus: true, ++ accessible_name: b.label, ++ child: new St.Label({ ++ text: b.label, ++ style_class: 'web-login-button-label', ++ }), ++ }); ++ ++ button.connectObject('clicked', () => { ++ if (b.needsLoading) ++ this._startLoading(); ++ b.action(); ++ }, this); ++ ++ button.default = b.default; ++ ++ this._buttonBox.add_child(button); ++ ++ this.bind_property('reactive', ++ button, 'reactive', ++ GObject.BindingFlags.SYNC_CREATE); ++ this.bind_property('reactive', ++ button, 'can_focus', ++ GObject.BindingFlags.SYNC_CREATE); ++ ++ return button; ++ } ++ ++ vfunc_key_focus_in() { ++ this._buttonBox.get_children().find(b => b.default)?.grab_key_focus(); ++ } ++ ++ update(params) { ++ const {message, url, code, buttons} = Params.parse(params, { ++ message: null, ++ url: null, ++ code: null, ++ buttons: [], ++ }); ++ ++ this.stopLoading(); ++ ++ this._webLoginPrompt.update({message, url, code}); ++ ++ this._updateButtons(buttons); ++ } ++ ++ _startLoading() { ++ this._webLoginPrompt.opacity = 128; ++ ++ this._buttonBox.get_children() ++ .filter(b => b !== this._cancelButton) ++ .forEach(b => { ++ b.opacity = 128; ++ b.can_focus = false; ++ b.reactive = false; ++ }); ++ this._cancelButton.grab_key_focus(); ++ ++ this._spinnerFrame.show(); ++ this._spinner.play(); ++ ++ this.isLoading = true; ++ this.emit('loading'); ++ } ++ ++ stopLoading() { ++ this._webLoginPrompt.opacity = 255; ++ ++ this._buttonBox.get_children() ++ .filter(b => b !== this._cancelButton) ++ .forEach(b => { ++ b.opacity = 255; ++ b.can_focus = this.reactive; ++ b.reactive = this.reactive; ++ }); ++ ++ this._spinnerFrame.hide(); ++ this._spinner.stop(); ++ ++ this.isLoading = false; ++ this.emit('loading'); ++ } ++}); ++ ++export var WebLoginIntro = GObject.registerClass( ++class WebLoginIntro extends St.Button { ++ constructor(params) { ++ const {message} = Params.parse(params, { ++ message: null, ++ }); ++ ++ const label = new St.Label({ ++ text: message, ++ style_class: 'web-login-button-label', ++ }); ++ ++ super({ ++ style_class: 'web-login-intro-button', ++ accessible_name: message, ++ button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE, ++ reactive: true, ++ can_focus: true, ++ child: label, ++ }); ++ } ++ ++ setMessage(message) { ++ this.child.text = message; ++ this.accessible_name = message; ++ } ++}); +diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml +index e8575a7b6e..40702ccabc 100644 +--- a/js/js-resources.gresource.xml ++++ b/js/js-resources.gresource.xml +@@ -15,6 +15,7 @@ + gdm/vmware.js + gdm/realmd.js + gdm/util.js ++ gdm/webLogin.js + + misc/config.js + misc/extensionUtils.js +diff --git a/js/ui/unlockDialog.js b/js/ui/unlockDialog.js +index e4c63c92cb..bebc7ad40a 100644 +--- a/js/ui/unlockDialog.js ++++ b/js/ui/unlockDialog.js +@@ -466,7 +466,8 @@ class UnlockDialogLayout extends Clutter.LayoutManager { + // Authentication Box + const dialog = container.get_parent(); + let stackY; +- if (dialog._activePage === dialog._clock) { ++ if (dialog._activePage === dialog._clock || ++ dialog._authPrompt?.webLoginActive) { + stackY = Math.min( + Math.floor(centerY - stackHeight / 2.0), + height - stackHeight - maxNotificationsHeight); +diff --git a/js/ui/userWidget.js b/js/ui/userWidget.js +index c376028af3..80bf795239 100644 +--- a/js/ui/userWidget.js ++++ b/js/ui/userWidget.js +@@ -247,4 +247,12 @@ class UserWidget extends St.BoxLayout { + _updateUser() { + this._avatar.update(); + } ++ ++ hideAvatar() { ++ this._avatar.hide(); ++ } ++ ++ showAvatar() { ++ this._avatar.show(); ++ } + }); +diff --git a/po/POTFILES.in b/po/POTFILES.in +index f2592f04c6..dfcd19cf0e 100644 +--- a/po/POTFILES.in ++++ b/po/POTFILES.in +@@ -10,6 +10,7 @@ js/gdm/authServicesLegacy.js + js/gdm/authServicesSSSDSwitchable.js + js/gdm/loginDialog.js + js/gdm/util.js ++js/gdm/webLogin.js + js/misc/systemActions.js + js/misc/util.js + js/portalHelper/main.js +-- +2.51.0 + + +From d77e9e49c2e8c0e5f03cddd2c30ad68041b74bd7 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Tue, 14 Jan 2025 07:31:59 -0500 +Subject: [PATCH 47/48] gdm: Add support for Smartcard in + authServicesSSSDSwitchable + +This allows selecting smartcard as a login method. + +While SSSD can inform for each user what mechanisms are available, it +can't know if a user has smartcard mechanism available until a valid smartcard +is inserted. + +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). +--- + js/gdm/authServicesSSSDSwitchable.js | 70 ++++++++++++++++++++++++++++ + 1 file changed, 70 insertions(+) + +diff --git a/js/gdm/authServicesSSSDSwitchable.js b/js/gdm/authServicesSSSDSwitchable.js +index 5369334b0a..e04814caf3 100644 +--- a/js/gdm/authServicesSSSDSwitchable.js ++++ b/js/gdm/authServicesSSSDSwitchable.js +@@ -15,11 +15,13 @@ const MechanismsStatus = { + export class AuthServicesSSSDSwitchable extends AuthServices { + static SupportedRoles = [ + Constants.PASSWORD_ROLE_NAME, ++ Constants.SMARTCARD_ROLE_NAME, + Constants.WEB_LOGIN_ROLE_NAME, + ]; + + static RoleToService = { + [Constants.PASSWORD_ROLE_NAME]: Constants.SWITCHABLE_AUTH_SERVICE_NAME, ++ [Constants.SMARTCARD_ROLE_NAME]: Constants.SWITCHABLE_AUTH_SERVICE_NAME, + [Constants.WEB_LOGIN_ROLE_NAME]: Constants.SWITCHABLE_AUTH_SERVICE_NAME, + }; + +@@ -33,6 +35,18 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + this._mechanismsStatus = MechanismsStatus.WAITING; + } + ++ _handleSelectChoice(serviceName, key) { ++ if (serviceName !== this._selectedMechanism?.serviceName) ++ return; ++ ++ if (this._selectedMechanism.role === Constants.SMARTCARD_ROLE_NAME) { ++ const certificates = this._selectedMechanism.certificates; ++ const cert = certificates.find(c => c.keyId === key); ++ this._selectedSmartcard = cert; ++ this.emit('ask-question', serviceName, cert.pinPrompt, true); ++ } ++ } ++ + async _handleAnswerQuery(serviceName, answer) { + if (serviceName !== this._selectedMechanism?.serviceName) + return; +@@ -48,6 +62,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + let response; + switch (this._selectedMechanism.role) { + case Constants.PASSWORD_ROLE_NAME: ++ case Constants.SMARTCARD_ROLE_NAME: + response = this._formatResponse(answer); + this._sendResponse(response); + break; +@@ -59,6 +74,9 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + case Constants.PASSWORD_ROLE_NAME: + this._startPasswordLogin(); + break; ++ case Constants.SMARTCARD_ROLE_NAME: ++ this._startSmartcardLogin(); ++ break; + case Constants.WEB_LOGIN_ROLE_NAME: + this._startWebLogin(); + break; +@@ -96,6 +114,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + this._priorityList = null; + this._enabledMechanisms = null; + this._selectedMechanism = null; ++ this._selectedSmartcard = null; + + this._resettingPassword = false; + +@@ -175,6 +194,14 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + }); + } + ++ _handleSmartcardChanged() { ++ if (!this._selectedMechanism || ++ !this._enabledMechanisms.some(({role}) => role === Constants.SMARTCARD_ROLE_NAME)) ++ return; ++ ++ this.emit('reset', {softReset: true, reuseEntryText: true}); ++ } ++ + _handleOnInfo(serviceName, info) { + if (!this._eventExpected()) + return; +@@ -243,6 +270,11 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + response = {password: answer}; + break; + } ++ case Constants.SMARTCARD_ROLE_NAME: { ++ const {tokenName, moduleName, keyId, label} = this._selectedSmartcard; ++ response = {pin: answer, tokenName, moduleName, keyId, label}; ++ break; ++ } + case Constants.WEB_LOGIN_ROLE_NAME: { + response = {}; + break; +@@ -286,6 +318,44 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + this.emit('ask-question', serviceName, prompt, true); + } + ++ _startSmartcardLogin() { ++ const {serviceName, certificates} = this._selectedMechanism; ++ ++ if (certificates.length === 1) { ++ this._selectedSmartcard = certificates[0]; ++ this.emit('ask-question', serviceName, certificates[0].pinPrompt, true); ++ return; ++ } ++ ++ const choiceList = {}; ++ for (const cert of certificates) ++ choiceList[cert.keyId] = this._parseCertInstruction(cert.certInstruction); ++ ++ const prompt = certificates.length === 0 ++ ? _('Insert Smartcard') ++ : _('Select Identity'); ++ ++ this.emit('show-choice-list', serviceName, prompt, choiceList); ++ } ++ ++ _parseCertInstruction(certInstruction) { ++ // Currently sssd can't split cert data in a more granular way ++ // so it's parsed manually here ++ const [description, subject] = certInstruction.split('\n'); ++ ++ const fields = subject?.split(',').map(f => f.trim()) ?? []; ++ const commonName = fields.find(f => f.startsWith('CN='))?.substring(3); ++ const organization = fields.find(f => f.startsWith('O='))?.substring(2); ++ ++ return { ++ title: commonName, ++ subtitle: description, ++ iconName: organization ? 'vcard-symbolic' : null, ++ iconTitle: organization ? _('Organization') : null, ++ iconSubtitle: organization, ++ }; ++ } ++ + _startWebLogin() { + const { + serviceName, +-- +2.51.0 + + +From ea0ce9e4db98aa4a29dc1d6511499e858c73d388 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Mon, 15 Sep 2025 16:36:42 +0200 +Subject: [PATCH 48/48] gdm: Add support for Passkey in + authServicesSSSDSwitchable + +This allows selecting passkey authentication mechanism. + +Use passkeyManager to detect when there's a passkey inserted or removed +to restart the service so the JSON protocol gives valid information +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/authServicesSSSDSwitchable.js | 46 +++++++++++++++++++++++++++- + js/gdm/constants.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 a62a3de901..0a94f493a2 100644 +--- a/js/gdm/authServices.js ++++ b/js/gdm/authServices.js +@@ -3,6 +3,7 @@ + import * as FingerprintManager from '../misc/fingerprintManager.js'; + import * as Params from '../misc/params.js'; + import {registerDestroyableType} from '../misc/signalTracker.js'; ++import * as PasskeyDeviceManager from '../misc/passkeyDeviceManager.js'; + import * as SmartcardManager from '../misc/smartcardManager.js'; + import {logErrorUnlessCancelled} from '../misc/errorUtils.js'; + import * as Util from './util.js'; +@@ -89,6 +90,7 @@ export class AuthServices extends GObject.Object { + this._cancellable = null; + + this._connectSmartcardManager(); ++ this._connectPasskeyDeviceManager(); + this._connectFingerprintManager(); + } + +@@ -230,6 +232,14 @@ export class AuthServices extends GObject.Object { + this); + } + ++ _connectPasskeyDeviceManager() { ++ this._passkeyDeviceManager = PasskeyDeviceManager.getPasskeyDeviceManager(); ++ this._passkeyDeviceManager.connectObject( ++ 'passkey-inserted', () => this._handlePasskeyChanged(), ++ 'passkey-removed', () => this._handlePasskeyChanged(), ++ this); ++ } ++ + _connectFingerprintManager() { + // Fingerprint can only work on lockscreen + if (!this._reauthOnly) +@@ -462,6 +472,8 @@ export class AuthServices extends GObject.Object { + + _handleSmartcardChanged() {} + ++ _handlePasskeyChanged() {} ++ + _handleFingerprintChanged() {} + + _handleOnInfo() {} +diff --git a/js/gdm/authServicesSSSDSwitchable.js b/js/gdm/authServicesSSSDSwitchable.js +index e04814caf3..de03dec1d7 100644 +--- a/js/gdm/authServicesSSSDSwitchable.js ++++ b/js/gdm/authServicesSSSDSwitchable.js +@@ -16,12 +16,14 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + static SupportedRoles = [ + Constants.PASSWORD_ROLE_NAME, + Constants.SMARTCARD_ROLE_NAME, ++ Constants.PASSKEY_ROLE_NAME, + Constants.WEB_LOGIN_ROLE_NAME, + ]; + + static RoleToService = { + [Constants.PASSWORD_ROLE_NAME]: Constants.SWITCHABLE_AUTH_SERVICE_NAME, + [Constants.SMARTCARD_ROLE_NAME]: Constants.SWITCHABLE_AUTH_SERVICE_NAME, ++ [Constants.PASSKEY_ROLE_NAME]: Constants.SWITCHABLE_AUTH_SERVICE_NAME, + [Constants.WEB_LOGIN_ROLE_NAME]: Constants.SWITCHABLE_AUTH_SERVICE_NAME, + }; + +@@ -66,6 +68,13 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + response = this._formatResponse(answer); + this._sendResponse(response); + break; ++ case Constants.PASSKEY_ROLE_NAME: ++ response = this._formatResponse(answer); ++ this._sendResponse(response); ++ ++ this.emit('show-choice-list', serviceName, ++ this._selectedMechanism.touchInstruction, {}); ++ break; + } + } + +@@ -77,6 +86,9 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + case Constants.SMARTCARD_ROLE_NAME: + this._startSmartcardLogin(); + break; ++ case Constants.PASSKEY_ROLE_NAME: ++ this._startPasskeyLogin(); ++ break; + case Constants.WEB_LOGIN_ROLE_NAME: + this._startWebLogin(); + break; +@@ -202,6 +214,14 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + this.emit('reset', {softReset: true, reuseEntryText: true}); + } + ++ _handlePasskeyChanged() { ++ if (!this._selectedMechanism || ++ !this._enabledMechanisms.some(({role}) => role === Constants.PASSKEY_ROLE_NAME)) ++ return; ++ ++ this.emit('reset', {softReset: true, reuseEntryText: true}); ++ } ++ + _handleOnInfo(serviceName, info) { + if (!this._eventExpected()) + return; +@@ -262,7 +282,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + } + + _formatResponse(answer) { +- const {role, id} = this._selectedMechanism; ++ const {role, id, kerberos, cryptoChallenge} = this._selectedMechanism; + + let response; + switch (role) { +@@ -275,6 +295,10 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + response = {pin: answer, tokenName, moduleName, keyId, label}; + break; + } ++ case Constants.PASSKEY_ROLE_NAME: { ++ response = {pin: answer, kerberos, cryptoChallenge}; ++ break; ++ } + case Constants.WEB_LOGIN_ROLE_NAME: { + response = {}; + break; +@@ -356,6 +380,26 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + }; + } + ++ _startPasskeyLogin() { ++ const { ++ serviceName, ++ keyConnected, initInstruction, ++ pinPrompt, pinAttempts, ++ } = this._selectedMechanism; ++ ++ if (!keyConnected) { ++ this.emit('show-choice-list', serviceName, initInstruction, {}); ++ return; ++ } ++ ++ this.emit('ask-question', serviceName, pinPrompt, true); ++ ++ if (pinAttempts <= 3 && pinAttempts > 0) { ++ const message = _('You have %d attempts left. If the passkey gets locked, you may not able to access your account.').format(pinAttempts); ++ this.emit('queue-message', serviceName, message, Util.MessageType.INFO); ++ } ++ } ++ + _startWebLogin() { + const { + serviceName, +diff --git a/js/gdm/constants.js b/js/gdm/constants.js +index e8ca48625a..7c48599e64 100644 +--- a/js/gdm/constants.js ++++ b/js/gdm/constants.js +@@ -3,6 +3,7 @@ + export const PASSWORD_ROLE_NAME = 'password'; + export const SMARTCARD_ROLE_NAME = 'smartcard'; + export const FINGERPRINT_ROLE_NAME = 'fingerprint'; ++export const PASSKEY_ROLE_NAME = 'passkey'; + export const WEB_LOGIN_ROLE_NAME = 'eidp'; + + export const PASSWORD_SERVICE_NAME = 'gdm-password'; +diff --git a/js/gdm/util.js b/js/gdm/util.js +index 6c160d323b..743fce11b7 100644 +--- a/js/gdm/util.js ++++ b/js/gdm/util.js +@@ -19,6 +19,7 @@ var LOGIN_SCREEN_SCHEMA = 'org.gnome.login-screen'; + var PASSWORD_AUTHENTICATION_KEY = 'enable-password-authentication'; + var FINGERPRINT_AUTHENTICATION_KEY = 'enable-fingerprint-authentication'; + var SMARTCARD_AUTHENTICATION_KEY = 'enable-smartcard-authentication'; ++var PASSKEY_AUTHENTICATION_KEY = 'enable-passkey-authentication'; + var SWITCHABLE_AUTHENTICATION_KEY = 'enable-switchable-authentication'; + var WEB_AUTHENTICATION_KEY = 'enable-web-authentication'; + var BANNER_MESSAGE_KEY = 'banner-message-enable'; +@@ -139,6 +140,7 @@ export function isSelectable(mechanism) { + switch (mechanism.role) { + case Constants.PASSWORD_ROLE_NAME: + case Constants.SMARTCARD_ROLE_NAME: ++ case Constants.PASSKEY_ROLE_NAME: + case Constants.WEB_LOGIN_ROLE_NAME: + return true; + case Constants.FINGERPRINT_ROLE_NAME: +@@ -449,6 +451,8 @@ var ShellUserVerifier = class { + enabledRoles.push(Constants.PASSWORD_ROLE_NAME); + if (this._settings.get_boolean(SMARTCARD_AUTHENTICATION_KEY)) + enabledRoles.push(Constants.SMARTCARD_ROLE_NAME); ++ if (this._settings.get_boolean(PASSKEY_AUTHENTICATION_KEY)) ++ enabledRoles.push(Constants.PASSKEY_ROLE_NAME); + if (this._settings.get_boolean(FINGERPRINT_AUTHENTICATION_KEY)) + enabledRoles.push(Constants.FINGERPRINT_ROLE_NAME); + if (this._settings.get_boolean(WEB_AUTHENTICATION_KEY)) +diff --git a/js/ui/unlockDialog.js b/js/ui/unlockDialog.js +index bebc7ad40a..654959190e 100644 +--- a/js/ui/unlockDialog.js ++++ b/js/ui/unlockDialog.js +@@ -398,6 +398,8 @@ class UnlockDialogClock extends St.BoxLayout { + + if (authMechanism?.role === GdmConstants.SMARTCARD_ROLE_NAME) + text = _('Insert smartcard'); ++ else if (authMechanism?.role === GdmConstants.PASSKEY_ROLE_NAME) ++ text = _('Insert security key'); + else if (this._seat.touch_mode) + text = _('Swipe up'); + else +-- +2.51.0 + diff --git a/SOURCES/0001-Update-generated-stylesheets.patch b/SOURCES/0001-Update-generated-stylesheets.patch index 4133dd6..04fe5a3 100644 --- a/SOURCES/0001-Update-generated-stylesheets.patch +++ b/SOURCES/0001-Update-generated-stylesheets.patch @@ -3,106 +3,138 @@ From: =?UTF-8?q?Florian=20M=C3=BCllner?= Date: Tue, 8 Feb 2022 14:18:04 -0500 --- - data/theme/gnome-shell-high-contrast.css | 36 ++++++++++++++++++++++++ - data/theme/gnome-shell.css | 36 ++++++++++++++++++++++++ - 2 files changed, 72 insertions(+) + data/theme/gnome-shell-high-contrast.css | 268 ++++++++++++++++++++++- + data/theme/gnome-shell.css | 268 ++++++++++++++++++++++- + 2 files changed, 524 insertions(+), 12 deletions(-) diff --git a/data/theme/gnome-shell-high-contrast.css b/data/theme/gnome-shell-high-contrast.css -index b73f407..90f363c 100644 +index b73f407..585d6c4 100644 --- a/data/theme/gnome-shell-high-contrast.css +++ b/data/theme/gnome-shell-high-contrast.css -@@ -1223,60 +1223,63 @@ StScrollBar { - -panel-corner-border-color: transparent; - -panel-corner-opacity: 1; - transition-duration: 250ms; } - #panel .panel-button { - font-weight: bold; - color: #ddd; - -natural-hpadding: 12px; - -minimum-hpadding: 6px; - transition-duration: 150ms; - border: 3px solid transparent; - border-radius: 99px; } - #panel .panel-button.clock-display .clock { - transition-duration: 150ms; - border: 3px solid transparent; +@@ -173,7 +173,7 @@ StEntry { + icon-size: 1.09em; + padding: 0 4px; } + StEntry StLabel.hint-text { +- margin-left: 2px; ++ margin-left: 8px; + color: rgba(255, 255, 255, 0.7); } + + /* Buttons */ +@@ -1237,9 +1237,9 @@ StScrollBar { border-radius: 99px; } #panel .panel-button:hover, #panel .panel-button:active, #panel .panel-button:overview, #panel .panel-button:focus, #panel .panel-button:checked { box-shadow: inset 0 0 0 100px rgba(255, 255, 255, 0.2); } - #panel .panel-button.clock-display:hover, #panel .panel-button.clock-display:active, #panel .panel-button.clock-display:overview, #panel .panel-button.clock-display:focus, #panel .panel-button.clock-display:checked { +- #panel .panel-button.clock-display:hover, #panel .panel-button.clock-display:active, #panel .panel-button.clock-display:overview, #panel .panel-button.clock-display:focus, #panel .panel-button.clock-display:checked { ++ #panel .panel-button:hover.clock-display, #panel .panel-button:active.clock-display, #panel .panel-button:overview.clock-display, #panel .panel-button:focus.clock-display, #panel .panel-button:checked.clock-display { box-shadow: none; } - #panel .panel-button.clock-display:hover .clock, #panel .panel-button.clock-display:active .clock, #panel .panel-button.clock-display:overview .clock, #panel .panel-button.clock-display:focus .clock, #panel .panel-button.clock-display:checked .clock { +- #panel .panel-button.clock-display:hover .clock, #panel .panel-button.clock-display:active .clock, #panel .panel-button.clock-display:overview .clock, #panel .panel-button.clock-display:focus .clock, #panel .panel-button.clock-display:checked .clock { ++ #panel .panel-button:hover.clock-display .clock, #panel .panel-button:active.clock-display .clock, #panel .panel-button:overview.clock-display .clock, #panel .panel-button:focus.clock-display .clock, #panel .panel-button:checked.clock-display .clock { box-shadow: inset 0 0 0 100px rgba(255, 255, 255, 0.2); } #panel .panel-button .system-status-icon { icon-size: 1.09em; - padding: 5px; - margin: 0 4px; } - #panel .panel-button .panel-status-indicators-box .system-status-icon, - #panel .panel-button .panel-status-menu-box .system-status-icon { +@@ -1250,13 +1250,16 @@ StScrollBar { margin: 0; } #panel .panel-button .app-menu-icon { -st-icon-style: symbolic; } +- #panel #panelActivities.panel-button { + #panel .panel-button .panel-logo-icon { + padding-right: .4em; + icon-size: 1em; } - #panel #panelActivities.panel-button { ++ #panel .panel-button#panelActivities { -natural-hpadding: 18px; } #panel.unlock-screen .panel-button:hover, #panel.unlock-screen .panel-button:active, #panel.unlock-screen .panel-button:overview, #panel.unlock-screen .panel-button:focus, #panel.unlock-screen .panel-button:checked, #panel.login-screen .panel-button:hover, #panel.login-screen .panel-button:active, #panel.login-screen .panel-button:overview, #panel.login-screen .panel-button:focus, #panel.login-screen .panel-button:checked, #panel:overview .panel-button:hover, #panel:overview .panel-button:active, #panel:overview .panel-button:overview, #panel:overview .panel-button:focus, #panel:overview .panel-button:checked { box-shadow: inset 0 0 0 100px rgba(255, 255, 255, 0.15); } - #panel.unlock-screen .panel-button.clock-display:hover, #panel.unlock-screen .panel-button.clock-display:active, #panel.unlock-screen .panel-button.clock-display:overview, #panel.unlock-screen .panel-button.clock-display:focus, #panel.unlock-screen .panel-button.clock-display:checked, #panel.login-screen .panel-button.clock-display:hover, #panel.login-screen .panel-button.clock-display:active, #panel.login-screen .panel-button.clock-display:overview, #panel.login-screen .panel-button.clock-display:focus, #panel.login-screen .panel-button.clock-display:checked, #panel:overview .panel-button.clock-display:hover, #panel:overview .panel-button.clock-display:active, #panel:overview .panel-button.clock-display:overview, #panel:overview .panel-button.clock-display:focus, #panel:overview .panel-button.clock-display:checked { +- #panel.unlock-screen .panel-button.clock-display:hover, #panel.unlock-screen .panel-button.clock-display:active, #panel.unlock-screen .panel-button.clock-display:overview, #panel.unlock-screen .panel-button.clock-display:focus, #panel.unlock-screen .panel-button.clock-display:checked, #panel.login-screen .panel-button.clock-display:hover, #panel.login-screen .panel-button.clock-display:active, #panel.login-screen .panel-button.clock-display:overview, #panel.login-screen .panel-button.clock-display:focus, #panel.login-screen .panel-button.clock-display:checked, #panel:overview .panel-button.clock-display:hover, #panel:overview .panel-button.clock-display:active, #panel:overview .panel-button.clock-display:overview, #panel:overview .panel-button.clock-display:focus, #panel:overview .panel-button.clock-display:checked { ++ #panel.unlock-screen .panel-button:hover.clock-display, #panel.unlock-screen .panel-button:active.clock-display, #panel.unlock-screen .panel-button:overview.clock-display, #panel.unlock-screen .panel-button:focus.clock-display, #panel.unlock-screen .panel-button:checked.clock-display, #panel.login-screen .panel-button:hover.clock-display, #panel.login-screen .panel-button:active.clock-display, #panel.login-screen .panel-button:overview.clock-display, #panel.login-screen .panel-button:focus.clock-display, #panel.login-screen .panel-button:checked.clock-display, #panel:overview .panel-button:hover.clock-display, #panel:overview .panel-button:active.clock-display, #panel:overview .panel-button:overview.clock-display, #panel:overview .panel-button:focus.clock-display, #panel:overview .panel-button:checked.clock-display { box-shadow: none; } - #panel.unlock-screen .panel-button.clock-display:hover .clock, #panel.unlock-screen .panel-button.clock-display:active .clock, #panel.unlock-screen .panel-button.clock-display:overview .clock, #panel.unlock-screen .panel-button.clock-display:focus .clock, #panel.unlock-screen .panel-button.clock-display:checked .clock, #panel.login-screen .panel-button.clock-display:hover .clock, #panel.login-screen .panel-button.clock-display:active .clock, #panel.login-screen .panel-button.clock-display:overview .clock, #panel.login-screen .panel-button.clock-display:focus .clock, #panel.login-screen .panel-button.clock-display:checked .clock, #panel:overview .panel-button.clock-display:hover .clock, #panel:overview .panel-button.clock-display:active .clock, #panel:overview .panel-button.clock-display:overview .clock, #panel:overview .panel-button.clock-display:focus .clock, #panel:overview .panel-button.clock-display:checked .clock { +- #panel.unlock-screen .panel-button.clock-display:hover .clock, #panel.unlock-screen .panel-button.clock-display:active .clock, #panel.unlock-screen .panel-button.clock-display:overview .clock, #panel.unlock-screen .panel-button.clock-display:focus .clock, #panel.unlock-screen .panel-button.clock-display:checked .clock, #panel.login-screen .panel-button.clock-display:hover .clock, #panel.login-screen .panel-button.clock-display:active .clock, #panel.login-screen .panel-button.clock-display:overview .clock, #panel.login-screen .panel-button.clock-display:focus .clock, #panel.login-screen .panel-button.clock-display:checked .clock, #panel:overview .panel-button.clock-display:hover .clock, #panel:overview .panel-button.clock-display:active .clock, #panel:overview .panel-button.clock-display:overview .clock, #panel:overview .panel-button.clock-display:focus .clock, #panel:overview .panel-button.clock-display:checked .clock { ++ #panel.unlock-screen .panel-button:hover.clock-display .clock, #panel.unlock-screen .panel-button:active.clock-display .clock, #panel.unlock-screen .panel-button:overview.clock-display .clock, #panel.unlock-screen .panel-button:focus.clock-display .clock, #panel.unlock-screen .panel-button:checked.clock-display .clock, #panel.login-screen .panel-button:hover.clock-display .clock, #panel.login-screen .panel-button:active.clock-display .clock, #panel.login-screen .panel-button:overview.clock-display .clock, #panel.login-screen .panel-button:focus.clock-display .clock, #panel.login-screen .panel-button:checked.clock-display .clock, #panel:overview .panel-button:hover.clock-display .clock, #panel:overview .panel-button:active.clock-display .clock, #panel:overview .panel-button:overview.clock-display .clock, #panel:overview .panel-button:focus.clock-display .clock, #panel:overview .panel-button:checked.clock-display .clock { box-shadow: inset 0 0 0 100px rgba(255, 255, 255, 0.15); } #panel .panel-status-indicators-box, #panel .panel-status-menu-box { - spacing: 2px; } - #panel .power-status.panel-status-indicators-box { - spacing: 0; } - #panel .screencast-indicator, - #panel .remote-access-indicator { - color: #f57900; } +@@ -1943,6 +1946,13 @@ StScrollBar { + border-radius: 8px; + padding: 6px; } - #appMenu { - spacing: 6px; } - #appMenu .label-shadow { - color: transparent; } - - #appMenu .panel-status-menu-box { - padding: 0 6px; - spacing: 6px; } - - /* Activities Ripple */ - .ripple-box { - background-color: rgba(158, 196, 235, 0.3); - box-shadow: 0 0 2px 2px #4a90d9; -@@ -2039,74 +2042,107 @@ StScrollBar { - width: 2.18em; ++.qr-code { ++ border-radius: 4px; ++ border-width: 1em; ++ background-color: #eeeeec; ++ border-color: #eeeeec; ++ color: #282828; } ++ + /* Login Dialog */ + .login-dialog-banner-view { + padding-top: 24px; +@@ -2028,11 +2038,15 @@ StScrollBar { + border-color: #184472; + background-color: #184472; + color: rgba(255, 255, 255, 0.7); } ++ .login-dialog .next-button, + .login-dialog .cancel-button, + .login-dialog .switch-user-button, ++ .login-dialog .login-dialog-auth-menu-button, + .login-dialog .login-dialog-session-list-button, ++ .unlock-dialog .next-button, + .unlock-dialog .cancel-button, + .unlock-dialog .switch-user-button, ++ .unlock-dialog .login-dialog-auth-menu-button, + .unlock-dialog .login-dialog-session-list-button { + padding: 0; + border-radius: 99px; +@@ -2040,18 +2054,51 @@ StScrollBar { height: 2.18em; border-color: #202020; background-color: #202020; } ++ .login-dialog .next-button StIcon, .login-dialog .cancel-button StIcon, .login-dialog .switch-user-button StIcon, ++ .login-dialog .login-dialog-auth-menu-button StIcon, .login-dialog .login-dialog-session-list-button StIcon, ++ .unlock-dialog .next-button StIcon, .unlock-dialog .cancel-button StIcon, .unlock-dialog .switch-user-button StIcon, ++ .unlock-dialog .login-dialog-auth-menu-button StIcon, .unlock-dialog .login-dialog-session-list-button StIcon { icon-size: 1.09em; } ++ .login-dialog .next-button, ++ .unlock-dialog .next-button { ++ background-color: transparent !important; } ++ .login-dialog .switch-user-button, ++ .unlock-dialog .switch-user-button { ++ min-width: 8.5em; ++ min-height: 2.5em; ++ font-size: 10pt; ++ font-weight: bold; } .login-dialog .caps-lock-warning-label, .login-dialog .login-dialog-message-warning, .unlock-dialog .caps-lock-warning-label, .unlock-dialog .login-dialog-message-warning { color: #eeeeec; } ++ .login-dialog .login-dialog-prompt-layout.web-login-active, ++ .unlock-dialog .login-dialog-prompt-layout.web-login-active { ++ width: 34.5em; } ++ .login-dialog .login-dialog-prompt-entry-area, ++ .unlock-dialog .login-dialog-prompt-entry-area { ++ margin: 0.5em 20px; } ++ .login-dialog .login-dialog-prompt-entry, ++ .unlock-dialog .login-dialog-prompt-entry { ++ border-radius: 12px; ++ padding-right: 2.5em; } ++ .login-dialog .login-dialog-default-button-well, ++ .unlock-dialog .login-dialog-default-button-well { ++ margin-right: 0.4em; } ++ ++.login-dialog-auth-menu-button-indicator-description { ++ color: #eeeeec; } ++ ++.login-dialog-bottom-button-group { ++ padding: 32px; ++ spacing: 16px; } .login-dialog-logo-bin { padding: 24px 0px; } - - .login-dialog-banner { - color: #d6d6d1; } - - .login-dialog-button-box { - width: 23em; - spacing: 5px; } - +@@ -2066,6 +2113,10 @@ StScrollBar { .login-dialog-message { text-align: center; } @@ -113,172 +145,353 @@ index b73f407..90f363c 100644 .login-dialog-user-selection-box { padding: 100px 0px; } - .login-dialog-not-listed-label { - padding-left: 2px; } - .login-dialog-not-listed-button:focus .login-dialog-not-listed-label, .login-dialog-not-listed-button:hover .login-dialog-not-listed-label { - color: #eeeeec; } - - .login-dialog-not-listed-label { - font-size: 10pt; - font-weight: bold; +@@ -2080,6 +2131,140 @@ StScrollBar { color: #a6a69b; padding-top: 1em; } ++.login-dialog-auth-menu-button-popup { ++ padding: 18px; ++ margin-right: 12px; } ++ .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-section { ++ padding-top: 6px; } ++ .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-section:first-child { ++ padding-top: 0; } ++ .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-section-label { ++ font-size: 10pt; ++ font-weight: bold; ++ color: #eeeeec; } ++ .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-icon { ++ color: #eeeeec; ++ icon-size: 1.09em; ++ margin-right: 8px; } ++ .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-box { ++ spacing: 3px; } ++ .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-box-name { ++ color: #eeeeec; ++ font-size: 11pt; ++ font-weight: bold; } ++ .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-box-description { ++ color: #eeeeec; ++ font-size: 10pt; } ++ ++.login-dialog-auth-menu-button-indicator { ++ background-color: transparent !important; ++ border-width: 0; ++ margin-bottom: 32px; ++ margin-left: 32px; } ++ .login-dialog-auth-menu-button-indicator .login-dialog-auth-menu-button-indicator-icons { ++ spacing: 18px; } ++ .login-dialog-auth-menu-button-indicator .login-dialog-auth-menu-button-indicator-icons .login-dialog-auth-menu-button-indicator-icon { ++ icon-size: 2em; } ++ .login-dialog-auth-menu-button-indicator .login-dialog-auth-menu-button-indicator-description { ++ font-size: 11pt; ++ margin-left: 12px; ++ min-width: 20em; } ++ ++.login-dialog-button-box { ++ height: 4em; } ++ +.login-dialog-auth-list-view { -+ -st-vfade-offset: 1em; } ++ -st-vfade-offset: 1em; ++ max-height: 13em; } + +.login-dialog-auth-list { -+ spacing: 6px; -+ margin-left: 2em; } ++ spacing: 9px; ++ width: 23em; ++ margin-left: 20px; } + -+.login-dialog-auth-list-title { -+ margin-left: 2em; } -+ -+.login-dialog-auth-list-item { ++.login-dialog .login-dialog-auth-list-title, ++.login-dialog .login-dialog-auth-list-item { + border-radius: 12px; -+ padding: 6px; + color: #a6a69b; } -+ .login-dialog-auth-list-item:focus, .login-dialog-auth-list-item:selected { ++ .login-dialog .login-dialog-auth-list-title:focus, .login-dialog .login-dialog-auth-list-title:selected, ++ .login-dialog .login-dialog-auth-list-item:focus, ++ .login-dialog .login-dialog-auth-list-item:selected { + background-color: #215d9c; + color: #ffffff; } + -+.login-dialog-auth-list-label { ++.login-dialog .login-dialog-auth-list-title { ++ background-color: rgba(238, 238, 236, 0.03) !important; ++ color: #eeeeec !important; ++ padding: 6px; ++ margin: 0.5em 20px; } ++ ++.login-dialog .login-dialog-auth-list-item { ++ min-height: 3em; ++ padding: 9px; ++ margin-bottom: 4px; ++ background-color: rgba(238, 238, 236, 0.06); } ++ ++.unlock-dialog .login-dialog-auth-list-item { ++ border: none; ++ color: #eeeeec; ++ background-color: rgba(238, 238, 236, 0.06); ++ border-radius: 12px; ++ min-height: 3em; ++ padding: 9px; ++ margin-bottom: 4px; } ++ .unlock-dialog .login-dialog-auth-list-item:hover, .unlock-dialog .login-dialog-auth-list-item:focus, .unlock-dialog .login-dialog-auth-list-item:selected { ++ background-color: rgba(238, 238, 236, 0.16); } ++ .unlock-dialog .login-dialog-auth-list-item:active { ++ background-color: rgba(238, 238, 236, 0.22); } ++ ++.unlock-dialog .login-dialog-auth-list-title { ++ background-color: transparent !important; ++ border: none; ++ color: #eeeeec !important; ++ padding: 6px; } ++ ++.login-dialog-auth-list-title-label { + font-size: 13pt; + font-weight: bold; -+ padding-left: 15px; } -+ .login-dialog-auth-list-label:ltr { -+ padding-left: 14px; -+ text-align: left; } -+ .login-dialog-auth-list-label:rtl { -+ padding-right: 14px; -+ text-align: right; } ++ padding: 6px; ++ text-align: center; } ++ ++.login-dialog-auth-list-item-title, ++.login-dialog-auth-list-item-subtitle { ++ font-size: 11pt; ++ text-align: center; ++ padding: 1.8px 0; } ++ ++.login-dialog-auth-list-item-title { ++ color: #eeeeec; ++ font-weight: bold; } ++ ++.login-dialog-auth-list-item-subtitle { ++ color: #bebeb6; ++ font-weight: 500; } ++ ++.login-dialog-item-icon { ++ width: 1.3em; ++ height: 1.3em; ++ color: #eeeeec; ++ padding: 4.2px 6px; ++ border-radius: 8px; } ++ .login-dialog-item-icon:hover { ++ background-color: #282828; } ++ .login-dialog-item-icon-popup.popup-menu { ++ min-width: 0; } ++ .login-dialog-item-icon-popup-box { ++ padding: 6px 0; } ++ .login-dialog-item-icon-popup-box .login-dialog-item-icon-popup-labels { ++ spacing: 3px; ++ font-weight: bold; ++ 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: #fff; } + .login-dialog-user-list-view { -st-vfade-offset: 1em; } - .login-dialog-user-list { - spacing: 12px; - width: 23em; } - .login-dialog-user-list:expanded .login-dialog-user-list-item:selected { - background-color: #215d9c; - color: #ffffff; } - .login-dialog-user-list:expanded .login-dialog-user-list-item:logged-in { - border-right: 2px solid #215d9c; } - - .login-dialog-user-list-item { - border-radius: 12px; - padding: 6px; - color: #a6a69b; } - .login-dialog-user-list-item:ltr .user-widget { - padding-right: 1em; } - .login-dialog-user-list-item:rtl .user-widget { - padding-left: 1em; } - .login-dialog-user-list-item .login-dialog-timed-login-indicator { - height: 2px; - margin-top: 6px; - background-color: #eeeeec; } - .login-dialog-user-list-item:focus .login-dialog-timed-login-indicator { - background-color: #ffffff; } - - .user-widget-label { - color: #eeeeec; } +@@ -2145,6 +2330,77 @@ StScrollBar { + font-size: 12pt; + padding-top: 1em; } ++.web-login-spinner { ++ background-color: rgba(0, 0, 0, 0.5); ++ border: 5px rgba(0, 0, 0, 0); ++ border-radius: 50px; } ++ ++.web-login-title-label { ++ font-size: 11pt; ++ color: #a6a69b; ++ text-align: center; } ++ ++.web-login-url-label { ++ font-size: 11pt; ++ font-family: monospace; ++ color: #eeeeec; ++ text-align: center; } ++ .web-login-url-label.web-login-url-label-long { ++ font-size: 9pt; } ++ ++.web-login-code-title-label { ++ font-size: 11pt; ++ color: #eeeeec; ++ text-align: center; } ++ ++.web-login-code-label { ++ font-size: 11pt; ++ color: #eeeeec; ++ font-weight: bold; ++ text-align: center; } ++ ++.web-login-prompt { ++ padding-top: 6px; ++ padding-bottom: 6px; ++ padding-left: 27px; ++ padding-right: 27px; ++ spacing: 1.75em; } ++ ++.web-login-button-label { ++ font-size: 13pt; ++ color: #eeeeec; ++ min-width: 12em; ++ text-align: center; ++ font-weight: bold; ++ padding-bottom: 0.1em; } ++ ++.login-dialog .web-login-intro-button, ++.login-dialog .web-login-prompt-button, ++.unlock-dialog .web-login-intro-button, ++.unlock-dialog .web-login-prompt-button { ++ border-radius: 32px !important; ++ padding: 9px 18px; ++ background-color: rgba(238, 238, 236, 0.06); } ++ ++.login-dialog .web-login-intro-button, ++.unlock-dialog .web-login-intro-button { ++ width: 10em; } ++ ++.login-dialog .web-login-prompt-button, ++.unlock-dialog .web-login-prompt-button { ++ width: 7em; ++ margin: 24px 8px; } ++ ++.login-dialog .web-login-intro-button:hover, .login-dialog .web-login-intro-button:focus, ++.login-dialog .web-login-prompt-button:hover, ++.login-dialog .web-login-prompt-button:focus { ++ background-color: #215d9c; } ++ ++.unlock-dialog .web-login-intro-button:hover, .unlock-dialog .web-login-intro-button:focus, ++.unlock-dialog .web-login-prompt-button:hover, ++.unlock-dialog .web-login-prompt-button:focus { ++ background-color: rgba(238, 238, 236, 0.16); } ++ + /* Screen Shield */ + .unlock-dialog-clock { + color: white; diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css -index f93819b..d3d7fc8 100644 +index f93819b..ff0c587 100644 --- a/data/theme/gnome-shell.css +++ b/data/theme/gnome-shell.css -@@ -1223,60 +1223,63 @@ StScrollBar { - -panel-corner-border-color: transparent; - -panel-corner-opacity: 1; - transition-duration: 250ms; } - #panel .panel-button { - font-weight: bold; - color: #ddd; - -natural-hpadding: 12px; - -minimum-hpadding: 6px; - transition-duration: 150ms; - border: 3px solid transparent; - border-radius: 99px; } - #panel .panel-button.clock-display .clock { - transition-duration: 150ms; - border: 3px solid transparent; +@@ -173,7 +173,7 @@ StEntry { + icon-size: 1.09em; + padding: 0 4px; } + StEntry StLabel.hint-text { +- margin-left: 2px; ++ margin-left: 8px; + color: rgba(238, 238, 236, 0.7); } + + /* Buttons */ +@@ -1237,9 +1237,9 @@ StScrollBar { border-radius: 99px; } #panel .panel-button:hover, #panel .panel-button:active, #panel .panel-button:overview, #panel .panel-button:focus, #panel .panel-button:checked { box-shadow: inset 0 0 0 100px rgba(255, 255, 255, 0.2); } - #panel .panel-button.clock-display:hover, #panel .panel-button.clock-display:active, #panel .panel-button.clock-display:overview, #panel .panel-button.clock-display:focus, #panel .panel-button.clock-display:checked { +- #panel .panel-button.clock-display:hover, #panel .panel-button.clock-display:active, #panel .panel-button.clock-display:overview, #panel .panel-button.clock-display:focus, #panel .panel-button.clock-display:checked { ++ #panel .panel-button:hover.clock-display, #panel .panel-button:active.clock-display, #panel .panel-button:overview.clock-display, #panel .panel-button:focus.clock-display, #panel .panel-button:checked.clock-display { box-shadow: none; } - #panel .panel-button.clock-display:hover .clock, #panel .panel-button.clock-display:active .clock, #panel .panel-button.clock-display:overview .clock, #panel .panel-button.clock-display:focus .clock, #panel .panel-button.clock-display:checked .clock { +- #panel .panel-button.clock-display:hover .clock, #panel .panel-button.clock-display:active .clock, #panel .panel-button.clock-display:overview .clock, #panel .panel-button.clock-display:focus .clock, #panel .panel-button.clock-display:checked .clock { ++ #panel .panel-button:hover.clock-display .clock, #panel .panel-button:active.clock-display .clock, #panel .panel-button:overview.clock-display .clock, #panel .panel-button:focus.clock-display .clock, #panel .panel-button:checked.clock-display .clock { box-shadow: inset 0 0 0 100px rgba(255, 255, 255, 0.2); } #panel .panel-button .system-status-icon { icon-size: 1.09em; - padding: 5px; - margin: 0 4px; } - #panel .panel-button .panel-status-indicators-box .system-status-icon, - #panel .panel-button .panel-status-menu-box .system-status-icon { +@@ -1250,13 +1250,16 @@ StScrollBar { margin: 0; } #panel .panel-button .app-menu-icon { -st-icon-style: symbolic; } +- #panel #panelActivities.panel-button { + #panel .panel-button .panel-logo-icon { + padding-right: .4em; + icon-size: 1em; } - #panel #panelActivities.panel-button { ++ #panel .panel-button#panelActivities { -natural-hpadding: 18px; } #panel.unlock-screen .panel-button:hover, #panel.unlock-screen .panel-button:active, #panel.unlock-screen .panel-button:overview, #panel.unlock-screen .panel-button:focus, #panel.unlock-screen .panel-button:checked, #panel.login-screen .panel-button:hover, #panel.login-screen .panel-button:active, #panel.login-screen .panel-button:overview, #panel.login-screen .panel-button:focus, #panel.login-screen .panel-button:checked, #panel:overview .panel-button:hover, #panel:overview .panel-button:active, #panel:overview .panel-button:overview, #panel:overview .panel-button:focus, #panel:overview .panel-button:checked { box-shadow: inset 0 0 0 100px rgba(255, 255, 255, 0.15); } - #panel.unlock-screen .panel-button.clock-display:hover, #panel.unlock-screen .panel-button.clock-display:active, #panel.unlock-screen .panel-button.clock-display:overview, #panel.unlock-screen .panel-button.clock-display:focus, #panel.unlock-screen .panel-button.clock-display:checked, #panel.login-screen .panel-button.clock-display:hover, #panel.login-screen .panel-button.clock-display:active, #panel.login-screen .panel-button.clock-display:overview, #panel.login-screen .panel-button.clock-display:focus, #panel.login-screen .panel-button.clock-display:checked, #panel:overview .panel-button.clock-display:hover, #panel:overview .panel-button.clock-display:active, #panel:overview .panel-button.clock-display:overview, #panel:overview .panel-button.clock-display:focus, #panel:overview .panel-button.clock-display:checked { +- #panel.unlock-screen .panel-button.clock-display:hover, #panel.unlock-screen .panel-button.clock-display:active, #panel.unlock-screen .panel-button.clock-display:overview, #panel.unlock-screen .panel-button.clock-display:focus, #panel.unlock-screen .panel-button.clock-display:checked, #panel.login-screen .panel-button.clock-display:hover, #panel.login-screen .panel-button.clock-display:active, #panel.login-screen .panel-button.clock-display:overview, #panel.login-screen .panel-button.clock-display:focus, #panel.login-screen .panel-button.clock-display:checked, #panel:overview .panel-button.clock-display:hover, #panel:overview .panel-button.clock-display:active, #panel:overview .panel-button.clock-display:overview, #panel:overview .panel-button.clock-display:focus, #panel:overview .panel-button.clock-display:checked { ++ #panel.unlock-screen .panel-button:hover.clock-display, #panel.unlock-screen .panel-button:active.clock-display, #panel.unlock-screen .panel-button:overview.clock-display, #panel.unlock-screen .panel-button:focus.clock-display, #panel.unlock-screen .panel-button:checked.clock-display, #panel.login-screen .panel-button:hover.clock-display, #panel.login-screen .panel-button:active.clock-display, #panel.login-screen .panel-button:overview.clock-display, #panel.login-screen .panel-button:focus.clock-display, #panel.login-screen .panel-button:checked.clock-display, #panel:overview .panel-button:hover.clock-display, #panel:overview .panel-button:active.clock-display, #panel:overview .panel-button:overview.clock-display, #panel:overview .panel-button:focus.clock-display, #panel:overview .panel-button:checked.clock-display { box-shadow: none; } - #panel.unlock-screen .panel-button.clock-display:hover .clock, #panel.unlock-screen .panel-button.clock-display:active .clock, #panel.unlock-screen .panel-button.clock-display:overview .clock, #panel.unlock-screen .panel-button.clock-display:focus .clock, #panel.unlock-screen .panel-button.clock-display:checked .clock, #panel.login-screen .panel-button.clock-display:hover .clock, #panel.login-screen .panel-button.clock-display:active .clock, #panel.login-screen .panel-button.clock-display:overview .clock, #panel.login-screen .panel-button.clock-display:focus .clock, #panel.login-screen .panel-button.clock-display:checked .clock, #panel:overview .panel-button.clock-display:hover .clock, #panel:overview .panel-button.clock-display:active .clock, #panel:overview .panel-button.clock-display:overview .clock, #panel:overview .panel-button.clock-display:focus .clock, #panel:overview .panel-button.clock-display:checked .clock { +- #panel.unlock-screen .panel-button.clock-display:hover .clock, #panel.unlock-screen .panel-button.clock-display:active .clock, #panel.unlock-screen .panel-button.clock-display:overview .clock, #panel.unlock-screen .panel-button.clock-display:focus .clock, #panel.unlock-screen .panel-button.clock-display:checked .clock, #panel.login-screen .panel-button.clock-display:hover .clock, #panel.login-screen .panel-button.clock-display:active .clock, #panel.login-screen .panel-button.clock-display:overview .clock, #panel.login-screen .panel-button.clock-display:focus .clock, #panel.login-screen .panel-button.clock-display:checked .clock, #panel:overview .panel-button.clock-display:hover .clock, #panel:overview .panel-button.clock-display:active .clock, #panel:overview .panel-button.clock-display:overview .clock, #panel:overview .panel-button.clock-display:focus .clock, #panel:overview .panel-button.clock-display:checked .clock { ++ #panel.unlock-screen .panel-button:hover.clock-display .clock, #panel.unlock-screen .panel-button:active.clock-display .clock, #panel.unlock-screen .panel-button:overview.clock-display .clock, #panel.unlock-screen .panel-button:focus.clock-display .clock, #panel.unlock-screen .panel-button:checked.clock-display .clock, #panel.login-screen .panel-button:hover.clock-display .clock, #panel.login-screen .panel-button:active.clock-display .clock, #panel.login-screen .panel-button:overview.clock-display .clock, #panel.login-screen .panel-button:focus.clock-display .clock, #panel.login-screen .panel-button:checked.clock-display .clock, #panel:overview .panel-button:hover.clock-display .clock, #panel:overview .panel-button:active.clock-display .clock, #panel:overview .panel-button:overview.clock-display .clock, #panel:overview .panel-button:focus.clock-display .clock, #panel:overview .panel-button:checked.clock-display .clock { box-shadow: inset 0 0 0 100px rgba(255, 255, 255, 0.15); } #panel .panel-status-indicators-box, #panel .panel-status-menu-box { - spacing: 2px; } - #panel .power-status.panel-status-indicators-box { - spacing: 0; } - #panel .screencast-indicator, - #panel .remote-access-indicator { - color: #f57900; } +@@ -1943,6 +1946,13 @@ StScrollBar { + border-radius: 8px; + padding: 6px; } - #appMenu { - spacing: 6px; } - #appMenu .label-shadow { - color: transparent; } - - #appMenu .panel-status-menu-box { - padding: 0 6px; - spacing: 6px; } - - /* Activities Ripple */ - .ripple-box { - background-color: rgba(188, 214, 246, 0.3); - box-shadow: 0 0 2px 2px #629fea; -@@ -2039,74 +2042,107 @@ StScrollBar { - width: 2.18em; ++.qr-code { ++ border-radius: 4px; ++ border-width: 1em; ++ background-color: #eeeeec; ++ border-color: #eeeeec; ++ color: #282828; } ++ + /* Login Dialog */ + .login-dialog-banner-view { + padding-top: 24px; +@@ -2028,11 +2038,15 @@ StScrollBar { + border-color: #15539e; + background-color: #15539e; + color: rgba(255, 255, 255, 0.7); } ++ .login-dialog .next-button, + .login-dialog .cancel-button, + .login-dialog .switch-user-button, ++ .login-dialog .login-dialog-auth-menu-button, + .login-dialog .login-dialog-session-list-button, ++ .unlock-dialog .next-button, + .unlock-dialog .cancel-button, + .unlock-dialog .switch-user-button, ++ .unlock-dialog .login-dialog-auth-menu-button, + .unlock-dialog .login-dialog-session-list-button { + padding: 0; + border-radius: 99px; +@@ -2040,18 +2054,51 @@ StScrollBar { height: 2.18em; border-color: #202020; background-color: #202020; } ++ .login-dialog .next-button StIcon, .login-dialog .cancel-button StIcon, .login-dialog .switch-user-button StIcon, ++ .login-dialog .login-dialog-auth-menu-button StIcon, .login-dialog .login-dialog-session-list-button StIcon, ++ .unlock-dialog .next-button StIcon, .unlock-dialog .cancel-button StIcon, .unlock-dialog .switch-user-button StIcon, ++ .unlock-dialog .login-dialog-auth-menu-button StIcon, .unlock-dialog .login-dialog-session-list-button StIcon { icon-size: 1.09em; } ++ .login-dialog .next-button, ++ .unlock-dialog .next-button { ++ background-color: transparent !important; } ++ .login-dialog .switch-user-button, ++ .unlock-dialog .switch-user-button { ++ min-width: 8.5em; ++ min-height: 2.5em; ++ font-size: 10pt; ++ font-weight: bold; } .login-dialog .caps-lock-warning-label, .login-dialog .login-dialog-message-warning, .unlock-dialog .caps-lock-warning-label, .unlock-dialog .login-dialog-message-warning { color: #eeeeec; } ++ .login-dialog .login-dialog-prompt-layout.web-login-active, ++ .unlock-dialog .login-dialog-prompt-layout.web-login-active { ++ width: 34.5em; } ++ .login-dialog .login-dialog-prompt-entry-area, ++ .unlock-dialog .login-dialog-prompt-entry-area { ++ margin: 0.5em 20px; } ++ .login-dialog .login-dialog-prompt-entry, ++ .unlock-dialog .login-dialog-prompt-entry { ++ border-radius: 12px; ++ padding-right: 2.5em; } ++ .login-dialog .login-dialog-default-button-well, ++ .unlock-dialog .login-dialog-default-button-well { ++ margin-right: 0.4em; } ++ ++.login-dialog-auth-menu-button-indicator-description { ++ color: #eeeeec; } ++ ++.login-dialog-bottom-button-group { ++ padding: 32px; ++ spacing: 16px; } .login-dialog-logo-bin { padding: 24px 0px; } - - .login-dialog-banner { - color: #d6d6d1; } - - .login-dialog-button-box { - width: 23em; - spacing: 5px; } - +@@ -2066,6 +2113,10 @@ StScrollBar { .login-dialog-message { text-align: center; } @@ -289,76 +502,225 @@ index f93819b..d3d7fc8 100644 .login-dialog-user-selection-box { padding: 100px 0px; } - .login-dialog-not-listed-label { - padding-left: 2px; } - .login-dialog-not-listed-button:focus .login-dialog-not-listed-label, .login-dialog-not-listed-button:hover .login-dialog-not-listed-label { - color: #eeeeec; } - - .login-dialog-not-listed-label { - font-size: 10pt; - font-weight: bold; +@@ -2080,6 +2131,140 @@ StScrollBar { color: #a6a69b; padding-top: 1em; } ++.login-dialog-auth-menu-button-popup { ++ padding: 18px; ++ margin-right: 12px; } ++ .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-section { ++ padding-top: 6px; } ++ .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-section:first-child { ++ padding-top: 0; } ++ .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-section-label { ++ font-size: 10pt; ++ font-weight: bold; ++ color: #eeeeec; } ++ .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-icon { ++ color: #eeeeec; ++ icon-size: 1.09em; ++ margin-right: 8px; } ++ .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-box { ++ spacing: 3px; } ++ .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-box-name { ++ color: #eeeeec; ++ font-size: 11pt; ++ font-weight: bold; } ++ .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-box-description { ++ color: #eeeeec; ++ font-size: 10pt; } ++ ++.login-dialog-auth-menu-button-indicator { ++ background-color: transparent !important; ++ border-width: 0; ++ margin-bottom: 32px; ++ margin-left: 32px; } ++ .login-dialog-auth-menu-button-indicator .login-dialog-auth-menu-button-indicator-icons { ++ spacing: 18px; } ++ .login-dialog-auth-menu-button-indicator .login-dialog-auth-menu-button-indicator-icons .login-dialog-auth-menu-button-indicator-icon { ++ icon-size: 2em; } ++ .login-dialog-auth-menu-button-indicator .login-dialog-auth-menu-button-indicator-description { ++ font-size: 11pt; ++ margin-left: 12px; ++ min-width: 20em; } ++ ++.login-dialog-button-box { ++ height: 4em; } ++ +.login-dialog-auth-list-view { -+ -st-vfade-offset: 1em; } ++ -st-vfade-offset: 1em; ++ max-height: 13em; } + +.login-dialog-auth-list { -+ spacing: 6px; -+ margin-left: 2em; } ++ spacing: 9px; ++ width: 23em; ++ margin-left: 20px; } + -+.login-dialog-auth-list-title { -+ margin-left: 2em; } -+ -+.login-dialog-auth-list-item { ++.login-dialog .login-dialog-auth-list-title, ++.login-dialog .login-dialog-auth-list-item { + border-radius: 12px; -+ padding: 6px; + color: #a6a69b; } -+ .login-dialog-auth-list-item:focus, .login-dialog-auth-list-item:selected { ++ .login-dialog .login-dialog-auth-list-title:focus, .login-dialog .login-dialog-auth-list-title:selected, ++ .login-dialog .login-dialog-auth-list-item:focus, ++ .login-dialog .login-dialog-auth-list-item:selected { + background-color: #1b6acb; + color: #fff; } + -+.login-dialog-auth-list-label { ++.login-dialog .login-dialog-auth-list-title { ++ background-color: rgba(238, 238, 236, 0.03) !important; ++ color: #eeeeec !important; ++ padding: 6px; ++ margin: 0.5em 20px; } ++ ++.login-dialog .login-dialog-auth-list-item { ++ min-height: 3em; ++ padding: 9px; ++ margin-bottom: 4px; ++ background-color: rgba(238, 238, 236, 0.06); } ++ ++.unlock-dialog .login-dialog-auth-list-item { ++ border: none; ++ color: #eeeeec; ++ background-color: rgba(238, 238, 236, 0.06); ++ border-radius: 12px; ++ min-height: 3em; ++ padding: 9px; ++ margin-bottom: 4px; } ++ .unlock-dialog .login-dialog-auth-list-item:hover, .unlock-dialog .login-dialog-auth-list-item:focus, .unlock-dialog .login-dialog-auth-list-item:selected { ++ background-color: rgba(238, 238, 236, 0.16); } ++ .unlock-dialog .login-dialog-auth-list-item:active { ++ background-color: rgba(238, 238, 236, 0.22); } ++ ++.unlock-dialog .login-dialog-auth-list-title { ++ background-color: transparent !important; ++ border: none; ++ color: #eeeeec !important; ++ padding: 6px; } ++ ++.login-dialog-auth-list-title-label { + font-size: 13pt; + font-weight: bold; -+ padding-left: 15px; } -+ .login-dialog-auth-list-label:ltr { -+ padding-left: 14px; -+ text-align: left; } -+ .login-dialog-auth-list-label:rtl { -+ padding-right: 14px; -+ text-align: right; } ++ padding: 6px; ++ text-align: center; } ++ ++.login-dialog-auth-list-item-title, ++.login-dialog-auth-list-item-subtitle { ++ font-size: 11pt; ++ text-align: center; ++ padding: 1.8px 0; } ++ ++.login-dialog-auth-list-item-title { ++ color: #eeeeec; ++ font-weight: bold; } ++ ++.login-dialog-auth-list-item-subtitle { ++ color: #bebeb6; ++ font-weight: 500; } ++ ++.login-dialog-item-icon { ++ width: 1.3em; ++ height: 1.3em; ++ color: #eeeeec; ++ padding: 4.2px 6px; ++ border-radius: 8px; } ++ .login-dialog-item-icon:hover { ++ background-color: #282828; } ++ .login-dialog-item-icon-popup.popup-menu { ++ min-width: 0; } ++ .login-dialog-item-icon-popup-box { ++ padding: 6px 0; } ++ .login-dialog-item-icon-popup-box .login-dialog-item-icon-popup-labels { ++ spacing: 3px; ++ font-weight: bold; ++ text-align: center; } ++ .login-dialog-item-icon-popup-box .login-dialog-item-icon-popup-labels > :first-child { ++ color: #a6a69b; ++ font-weight: 500; } ++ .login-dialog-item-icon-popup-box .login-dialog-item-icon-popup-labels > :last-child { ++ color: #eeeeec; } + .login-dialog-user-list-view { -st-vfade-offset: 1em; } - .login-dialog-user-list { - spacing: 12px; - width: 23em; } - .login-dialog-user-list:expanded .login-dialog-user-list-item:selected { - background-color: #1b6acb; - color: #fff; } - .login-dialog-user-list:expanded .login-dialog-user-list-item:logged-in { - border-right: 2px solid #1b6acb; } - - .login-dialog-user-list-item { - border-radius: 12px; - padding: 6px; - color: #a6a69b; } - .login-dialog-user-list-item:ltr .user-widget { - padding-right: 1em; } - .login-dialog-user-list-item:rtl .user-widget { - padding-left: 1em; } - .login-dialog-user-list-item .login-dialog-timed-login-indicator { - height: 2px; - margin-top: 6px; - background-color: #eeeeec; } - .login-dialog-user-list-item:focus .login-dialog-timed-login-indicator { - background-color: #fff; } - - .user-widget-label { - color: #eeeeec; } +@@ -2145,6 +2330,77 @@ StScrollBar { + font-size: 12pt; + padding-top: 1em; } ++.web-login-spinner { ++ background-color: rgba(0, 0, 0, 0.5); ++ border: 5px rgba(0, 0, 0, 0); ++ border-radius: 50px; } ++ ++.web-login-title-label { ++ font-size: 11pt; ++ color: #a6a69b; ++ text-align: center; } ++ ++.web-login-url-label { ++ font-size: 11pt; ++ font-family: monospace; ++ color: #eeeeec; ++ text-align: center; } ++ .web-login-url-label.web-login-url-label-long { ++ font-size: 9pt; } ++ ++.web-login-code-title-label { ++ font-size: 11pt; ++ color: #eeeeec; ++ text-align: center; } ++ ++.web-login-code-label { ++ font-size: 11pt; ++ color: #eeeeec; ++ font-weight: bold; ++ text-align: center; } ++ ++.web-login-prompt { ++ padding-top: 6px; ++ padding-bottom: 6px; ++ padding-left: 27px; ++ padding-right: 27px; ++ spacing: 1.75em; } ++ ++.web-login-button-label { ++ font-size: 13pt; ++ color: #eeeeec; ++ min-width: 12em; ++ text-align: center; ++ font-weight: bold; ++ padding-bottom: 0.1em; } ++ ++.login-dialog .web-login-intro-button, ++.login-dialog .web-login-prompt-button, ++.unlock-dialog .web-login-intro-button, ++.unlock-dialog .web-login-prompt-button { ++ border-radius: 32px !important; ++ padding: 9px 18px; ++ background-color: rgba(238, 238, 236, 0.06); } ++ ++.login-dialog .web-login-intro-button, ++.unlock-dialog .web-login-intro-button { ++ width: 10em; } ++ ++.login-dialog .web-login-prompt-button, ++.unlock-dialog .web-login-prompt-button { ++ width: 7em; ++ margin: 24px 8px; } ++ ++.login-dialog .web-login-intro-button:hover, .login-dialog .web-login-intro-button:focus, ++.login-dialog .web-login-prompt-button:hover, ++.login-dialog .web-login-prompt-button:focus { ++ background-color: #1b6acb; } ++ ++.unlock-dialog .web-login-intro-button:hover, .unlock-dialog .web-login-intro-button:focus, ++.unlock-dialog .web-login-prompt-button:hover, ++.unlock-dialog .web-login-prompt-button:focus { ++ background-color: rgba(238, 238, 236, 0.16); } ++ + /* Screen Shield */ + .unlock-dialog-clock { + color: white; -- -2.34.1 +2.51.0 diff --git a/SOURCES/0001-gdm-util-Early-initialize-all-internal-properties.patch b/SOURCES/0001-gdm-util-Early-initialize-all-internal-properties.patch new file mode 100644 index 0000000..de3eb38 --- /dev/null +++ b/SOURCES/0001-gdm-util-Early-initialize-all-internal-properties.patch @@ -0,0 +1,1008 @@ +From f1c0f65075b839b59e082d1cc060f9bc80ac8574 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= +Date: Fri, 21 Oct 2022 19:09:09 +0200 +Subject: [PATCH 01/13] gdm/util: Early initialize all internal properties + +These may be used later by methods called by constructors, so ensure +that they're all defined early. + +Part-of: +--- + js/gdm/util.js | 21 ++++++++++++--------- + 1 file changed, 12 insertions(+), 9 deletions(-) + +diff --git a/js/gdm/util.js b/js/gdm/util.js +index 3f32740..ecc05fa 100644 +--- a/js/gdm/util.js ++++ b/js/gdm/util.js +@@ -142,10 +142,22 @@ var ShellUserVerifier = class { + this._reauthOnly = params.reauthenticationOnly; + + this._client = client; ++ this._cancellable = null; + + this._defaultService = null; + this._preemptingService = null; + ++ this._messageQueue = []; ++ this._messageQueueTimeoutId = 0; ++ ++ this._failCounter = 0; ++ this._unavailableServices = new Set(); ++ ++ this._credentialManagers = {}; ++ ++ this.reauthenticating = false; ++ this.smartcardDetected = false; ++ + this._settings = new Gio.Settings({ schema_id: LOGIN_SCREEN_SCHEMA }); + this._settings.connect('changed', + this._updateDefaultService.bind(this)); +@@ -172,14 +184,6 @@ var ShellUserVerifier = class { + this._smartcardRemovedId = this._smartcardManager.connect('smartcard-removed', + this._checkForSmartcard.bind(this)); + +- this._messageQueue = []; +- this._messageQueueTimeoutId = 0; +- this.reauthenticating = false; +- +- this._failCounter = 0; +- this._unavailableServices = new Set(); +- +- this._credentialManagers = {}; + this._credentialManagers[OVirt.SERVICE_NAME] = OVirt.getOVirtCredentialsManager(); + this._credentialManagers[Vmware.SERVICE_NAME] = Vmware.getVmwareCredentialsManager(); + +-- +2.51.0 + + +From 9af029e968a6ad369ae8f73fb0e7d73f37ebe677 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= +Date: Fri, 21 Oct 2022 17:47:15 +0200 +Subject: [PATCH 02/13] gdm/util: Keep track of started services that are + currently active + +If we have a late activation of a service backend we may need to check +whether it has been already started, and in case it has not, we can try +loading it. + +So rely on gdm to see what service has been started, instead of handling +it manually on our side only. + +Part-of: +--- + js/gdm/util.js | 12 ++++++++++++ + 1 file changed, 12 insertions(+) + +diff --git a/js/gdm/util.js b/js/gdm/util.js +index ecc05fa..7694b6f 100644 +--- a/js/gdm/util.js ++++ b/js/gdm/util.js +@@ -151,6 +151,7 @@ var ShellUserVerifier = class { + this._messageQueueTimeoutId = 0; + + this._failCounter = 0; ++ this._activeServices = new Set(); + this._unavailableServices = new Set(); + + this._credentialManagers = {}; +@@ -257,6 +258,7 @@ var ShellUserVerifier = class { + + this._clearUserVerifier(); + this._clearMessageQueue(); ++ this._activeServices.clear(); + } + + destroy() { +@@ -513,6 +515,8 @@ var ShellUserVerifier = class { + this._signalIds.push(id); + id = this._userVerifier.connect('secret-info-query', this._onSecretInfoQuery.bind(this)); + this._signalIds.push(id); ++ id = this._userVerifier.connect('conversation-started', this._onConversationStarted.bind(this)); ++ this._signalIds.push(id); + id = this._userVerifier.connect('conversation-stopped', this._onConversationStopped.bind(this)); + this._signalIds.push(id); + id = this._userVerifier.connect('service-unavailable', this._onServiceUnavailable.bind(this)); +@@ -573,6 +577,7 @@ var ShellUserVerifier = class { + async _startService(serviceName) { + this._hold.acquire(); + try { ++ this._activeServices.add(serviceName); + if (this._userName) { + await this._userVerifier.call_begin_verification_for_user( + serviceName, this._userName, this._cancellable); +@@ -581,6 +586,7 @@ var ShellUserVerifier = class { + serviceName, this._cancellable); + } + } catch (e) { ++ this._activeServices.delete(serviceName); + if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + return; + if (!this.serviceIsForeground(serviceName)) { +@@ -696,6 +702,7 @@ var ShellUserVerifier = class { + _onReset() { + // Clear previous attempts to authenticate + this._failCounter = 0; ++ this._activeServices.clear(); + this._unavailableServices.clear(); + this._updateDefaultService(); + +@@ -776,7 +783,13 @@ var ShellUserVerifier = class { + this._queueMessage(serviceName, errorMessage, MessageType.ERROR); + } + ++ _onConversationStarted(client, serviceName) { ++ this._activeServices.add(serviceName); ++ } ++ + _onConversationStopped(client, serviceName) { ++ this._activeServices.delete(serviceName); ++ + // If the login failed with the preauthenticated oVirt credentials + // then discard the credentials and revert to default authentication + // mechanism. +-- +2.51.0 + + +From dc4f0f9053b0db3d064be5e8e74699a3cd11209f Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= +Date: Fri, 21 Oct 2022 19:05:04 +0200 +Subject: [PATCH 03/13] gdm/util: Restart auth if default service changed + +If a default service changed while the previous one was active we need to +reset the authentication so that we set back to the expected one. + +Part-of: +--- + js/gdm/util.js | 7 +++++++ + 1 file changed, 7 insertions(+) + +diff --git a/js/gdm/util.js b/js/gdm/util.js +index e3c6e1b..83c7d43 100644 +--- a/js/gdm/util.js ++++ b/js/gdm/util.js +@@ -559,6 +559,8 @@ var ShellUserVerifier = class { + } + + _updateDefaultService() { ++ const oldDefaultService = this._defaultService; ++ + if (this._smartcardManager?.loggedInWithToken()) + this._defaultService = SMARTCARD_SERVICE_NAME; + else if (this._settings.get_boolean(PASSWORD_AUTHENTICATION_KEY)) +@@ -572,6 +574,11 @@ var ShellUserVerifier = class { + log("no authentication service is enabled, using password authentication"); + this._defaultService = PASSWORD_SERVICE_NAME; + } ++ ++ if (oldDefaultService && ++ oldDefaultService !== this._defaultService && ++ this._activeServices.has(oldDefaultService)) ++ this._cancelAndReset(); + } + + async _startService(serviceName) { +-- +2.51.0 + + +From 9f3a9f8f2c619e9b9f49e818434dc6569eea66d9 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= +Date: Tue, 25 Oct 2022 03:51:30 +0200 +Subject: [PATCH 04/13] gdm/util: Do not use gjs GDBus proxy wrapper for fprint + manager + +It only adds more complexity, while we can handle it all manually quite +easier now. + +Part-of: +--- + js/gdm/util.js | 19 +++++++++++-------- + 1 file changed, 11 insertions(+), 8 deletions(-) + +diff --git a/js/gdm/util.js b/js/gdm/util.js +index 7c5a084..7babc26 100644 +--- a/js/gdm/util.js ++++ b/js/gdm/util.js +@@ -13,8 +13,8 @@ const { loadInterfaceXML } = imports.misc.fileUtils; + const Params = imports.misc.params; + const SmartcardManager = imports.misc.smartcardManager; + +-const FprintManagerIface = loadInterfaceXML('net.reactivated.Fprint.Manager'); +-const FprintManagerProxy = Gio.DBusProxy.makeProxyWrapper(FprintManagerIface); ++const FprintManagerInfo = Gio.DBusInterfaceInfo.new_for_xml( ++ loadInterfaceXML('net.reactivated.Fprint.Manager')); + const FprintDeviceIface = loadInterfaceXML('net.reactivated.Fprint.Device'); + const FprintDeviceProxy = Gio.DBusProxy.makeProxyWrapper(FprintDeviceIface); + +@@ -163,12 +163,15 @@ var ShellUserVerifier = class { + this._settings.connect('changed', + this._updateDefaultService.bind(this)); + +- this._fprintManager = new FprintManagerProxy(Gio.DBus.system, +- 'net.reactivated.Fprint', +- '/net/reactivated/Fprint/Manager', +- null, +- null, +- Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES); ++ this._fprintManager = new Gio.DBusProxy({ ++ g_connection: Gio.DBus.system, ++ g_name: 'net.reactivated.Fprint', ++ g_object_path: '/net/reactivated/Fprint/Manager', ++ g_interface_name: FprintManagerInfo.name, ++ g_interface_info: FprintManagerInfo, ++ g_flags: Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES, ++ }); ++ this._fprintManager.init(null); + this._smartcardManager = SmartcardManager.getSmartcardManager(); + + // We check for smartcards right away, since an inserted smartcard +-- +2.51.0 + + +From ea3731de3828827bb632ab7eba64cf4d0e85abc9 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= +Date: Tue, 1 Mar 2022 11:57:20 +0100 +Subject: [PATCH 05/13] gdm/util: Only initialize fingerprint and smart card + managers if needed + +There's no point to initialize the fprint proxy and the smartcard +manager if they are disabled in authentication settings, so just avoid +initializing them, but at the same time this implies tracking of user +changes and so: + - If a new service has been enabled, we initialize it + - If a service has been disabled we destroy it and reset the + authentication if such service was currently active + +In both cases we do update the default service. + +Part-of: +--- + js/gdm/util.js | 100 +++++++++++++++++++++++++++++++++++-------------- + 1 file changed, 72 insertions(+), 28 deletions(-) + +diff --git a/js/gdm/util.js b/js/gdm/util.js +index 7babc26..f2041cb 100644 +--- a/js/gdm/util.js ++++ b/js/gdm/util.js +@@ -160,34 +160,10 @@ var ShellUserVerifier = class { + this.smartcardDetected = false; + + this._settings = new Gio.Settings({ schema_id: LOGIN_SCREEN_SCHEMA }); +- this._settings.connect('changed', +- this._updateDefaultService.bind(this)); +- +- this._fprintManager = new Gio.DBusProxy({ +- g_connection: Gio.DBus.system, +- g_name: 'net.reactivated.Fprint', +- g_object_path: '/net/reactivated/Fprint/Manager', +- g_interface_name: FprintManagerInfo.name, +- g_interface_info: FprintManagerInfo, +- g_flags: Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES, +- }); +- this._fprintManager.init(null); +- this._smartcardManager = SmartcardManager.getSmartcardManager(); +- +- // We check for smartcards right away, since an inserted smartcard +- // at startup should result in immediately initiating authentication. +- // This is different than fingerprint readers, where we only check them +- // after a user has been picked. +- this.smartcardDetected = false; +- this._checkForSmartcard(); +- ++ this._settings.connect('changed', () => this._onSettingsChanged()); ++ this._updateEnabledServices(); + this._updateDefaultService(); + +- this._smartcardInsertedId = this._smartcardManager.connect('smartcard-inserted', +- this._checkForSmartcard.bind(this)); +- this._smartcardRemovedId = this._smartcardManager.connect('smartcard-removed', +- this._checkForSmartcard.bind(this)); +- + this._credentialManagers[OVirt.SERVICE_NAME] = OVirt.getOVirtCredentialsManager(); + this._credentialManagers[Vmware.SERVICE_NAME] = Vmware.getVmwareCredentialsManager(); + +@@ -270,10 +246,12 @@ var ShellUserVerifier = class { + this._settings.run_dispose(); + this._settings = null; + +- this._smartcardManager.disconnect(this._smartcardInsertedId); +- this._smartcardManager.disconnect(this._smartcardRemovedId); ++ this._smartcardManager?.disconnect(this._smartcardInsertedId); ++ this._smartcardManager?.disconnect(this._smartcardRemovedId); + this._smartcardManager = null; + ++ this._fingerprintManager = null; ++ + for (let service in this._credentialManagers) { + let credentialManager = this._credentialManagers[service]; + credentialManager.disconnect(credentialManager._authenticatedSignalId); +@@ -388,11 +366,25 @@ var ShellUserVerifier = class { + this.emit('show-message', null, null, MessageType.NONE); + } + ++ _initFingerprintManager() { ++ if (this._fprintManager) ++ return; ++ ++ this._fprintManager = new Gio.DBusProxy({ ++ g_connection: Gio.DBus.system, ++ g_name: 'net.reactivated.Fprint', ++ g_object_path: '/net/reactivated/Fprint/Manager', ++ g_interface_name: FprintManagerInfo.name, ++ g_interface_info: FprintManagerInfo, ++ g_flags: Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES, ++ }); ++ this._fprintManager.init(null); ++ } ++ + _checkForFingerprintReader() { + this._fingerprintReaderType = FingerprintReaderType.NONE; + +- if (!this._settings.get_boolean(FINGERPRINT_AUTHENTICATION_KEY) || +- this._fprintManager == null) { ++ if (!this._fprintManager) { + this._updateDefaultService(); + return; + } +@@ -419,6 +411,25 @@ var ShellUserVerifier = class { + this.emit('credential-manager-authenticated'); + } + ++ _initSmartcardManager() { ++ if (this._smartcardManager) ++ return; ++ ++ this._smartcardManager = SmartcardManager.getSmartcardManager(); ++ ++ // We check for smartcards right away, since an inserted smartcard ++ // at startup should result in immediately initiating authentication. ++ // This is different than fingerprint readers, where we only check them ++ // after a user has been picked. ++ this.smartcardDetected = false; ++ this._checkForSmartcard(); ++ ++ this._smartcardInsertedId = this._smartcardManager.connect('smartcard-inserted', ++ this._checkForSmartcard.bind(this)); ++ this._smartcardRemovedId = this._smartcardManager.connect('smartcard-removed', ++ this._checkForSmartcard.bind(this)); ++ } ++ + _checkForSmartcard() { + let smartcardDetected; + +@@ -561,6 +572,39 @@ var ShellUserVerifier = class { + serviceName === FINGERPRINT_SERVICE_NAME; + } + ++ _onSettingsChanged() { ++ this._updateEnabledServices(); ++ this._updateDefaultService(); ++ } ++ ++ _updateEnabledServices() { ++ let needsReset = false; ++ ++ if (this._settings.get_boolean(FINGERPRINT_AUTHENTICATION_KEY)) { ++ this._initFingerprintManager(); ++ } else if (this._fingerprintManager) { ++ this._fingerprintManager = null; ++ this._fingerprintReaderType = FingerprintReaderType.NONE; ++ ++ if (this._activeServices.has(FINGERPRINT_SERVICE_NAME)) ++ needsReset = true; ++ } ++ ++ if (this._settings.get_boolean(SMARTCARD_AUTHENTICATION_KEY)) { ++ this._initSmartcardManager(); ++ } else if (this._smartcardManager) { ++ this._smartcardManager.disconnect(this._smartcardInsertedId); ++ this._smartcardManager.disconnect(this._smartcardRemovedId); ++ this._smartcardManager = null; ++ ++ if (this._activeServices.has(SMARTCARD_SERVICE_NAME)) ++ needsReset = true; ++ } ++ ++ if (needsReset) ++ this._cancelAndReset(); ++ } ++ + _updateDefaultService() { + const oldDefaultService = this._defaultService; + +@@ -568,7 +612,7 @@ var ShellUserVerifier = class { + this._defaultService = SMARTCARD_SERVICE_NAME; + else if (this._settings.get_boolean(PASSWORD_AUTHENTICATION_KEY)) + this._defaultService = PASSWORD_SERVICE_NAME; +- else if (this._settings.get_boolean(SMARTCARD_AUTHENTICATION_KEY)) ++ else if (this._smartcardManager) + this._defaultService = SMARTCARD_SERVICE_NAME; + else if (this._fingerprintReaderType !== FingerprintReaderType.NONE) + this._defaultService = FINGERPRINT_SERVICE_NAME; +-- +2.51.0 + + +From 0d24563296a5a53712fe72c8995a1322fd5b4121 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= +Date: Tue, 25 Oct 2022 04:47:08 +0200 +Subject: [PATCH 06/13] gdm/util: Do not handle fingerprint async errors in + call + +It's better to do this at caller level so that we have more control of +what to do on errors. + +Part-of: +--- + js/gdm/util.js | 26 ++++++++++++-------------- + 1 file changed, 12 insertions(+), 14 deletions(-) + +diff --git a/js/gdm/util.js b/js/gdm/util.js +index f2041cb..569f3fd 100644 +--- a/js/gdm/util.js ++++ b/js/gdm/util.js +@@ -197,7 +197,7 @@ var ShellUserVerifier = class { + this._userName = userName; + this.reauthenticating = false; + +- this._checkForFingerprintReader(); ++ this._checkForFingerprintReader().catch(logError); + + // If possible, reauthenticate an already running session, + // so any session specific credentials get updated appropriately +@@ -381,7 +381,7 @@ var ShellUserVerifier = class { + this._fprintManager.init(null); + } + +- _checkForFingerprintReader() { ++ async _checkForFingerprintReader() { + this._fingerprintReaderType = FingerprintReaderType.NONE; + + if (!this._fprintManager) { +@@ -389,21 +389,17 @@ var ShellUserVerifier = class { + return; + } + +- this._fprintManager.GetDefaultDeviceRemote(Gio.DBusCallFlags.NONE, this._cancellable, +- (params, error) => { +- if (!error && params) { +- const [device] = params; +- const fprintDeviceProxy = new FprintDeviceProxy(Gio.DBus.system, +- 'net.reactivated.Fprint', +- device); +- const fprintDeviceType = fprintDeviceProxy['scan-type']; +- +- this._fingerprintReaderType = fprintDeviceType === 'swipe' +- ? FingerprintReaderType.SWIPE +- : FingerprintReaderType.PRESS; +- this._updateDefaultService(); +- } +- }); ++ const [device] = await this._fprintManager.GetDefaultDeviceAsync( ++ Gio.DBusCallFlags.NONE, this._cancellable); ++ const fprintDeviceProxy = new FprintDeviceProxy(Gio.DBus.system, ++ 'net.reactivated.Fprint', ++ device); ++ const fprintDeviceType = fprintDeviceProxy['scan-type']; ++ ++ this._fingerprintReaderType = fprintDeviceType === 'swipe' ++ ? FingerprintReaderType.SWIPE ++ : FingerprintReaderType.PRESS; ++ this._updateDefaultService(); + } + + _onCredentialManagerAuthenticated(credentialManager, _token) { +-- +2.51.0 + + +From d5ca108a65488c369929cfc360b1532e953c7286 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= +Date: Tue, 12 Apr 2022 16:38:40 +0200 +Subject: [PATCH 07/13] gdm/util: Start fingerprint verification once it's + available + +If fingerprint service is not replying fast enough to our async request, +authentication is started but the fingerprint service is never started. + +So, in case the fingerprint type information is received after that the +authentication has been started, let's start the service. + +Part-of: +--- + js/gdm/util.js | 12 +++++++++++- + 1 file changed, 11 insertions(+), 1 deletion(-) + +diff --git a/js/gdm/util.js b/js/gdm/util.js +index 6731200f66..24593f6b5c 100644 +--- a/js/gdm/util.js ++++ b/js/gdm/util.js +@@ -366,6 +366,13 @@ export class ShellUserVerifier extends Signals.EventEmitter { + ? FingerprintReaderType.SWIPE + : FingerprintReaderType.PRESS; + this._updateDefaultService(); ++ ++ if (this._userVerifier && ++ !this._activeServices.has(FINGERPRINT_SERVICE_NAME)) { ++ if (!this._hold?.isAcquired()) ++ this._hold = new Batch.Hold(); ++ await this._maybeStartFingerprintVerification(); ++ } + } + + _onCredentialManagerAuthenticated(credentialManager, _token) { +@@ -617,11 +624,14 @@ export class ShellUserVerifier extends Signals.EventEmitter { + + _beginVerification() { + this._startService(this._getForegroundService()); ++ this._maybeStartFingerprintVerification().catch(logError); ++ } + ++ async _maybeStartFingerprintVerification() { + if (this._userName && + this._fingerprintReaderType !== FingerprintReaderType.NONE && + !this.serviceIsForeground(FINGERPRINT_SERVICE_NAME)) +- this._startService(FINGERPRINT_SERVICE_NAME); ++ await this._startService(FINGERPRINT_SERVICE_NAME); + } + + _onChoiceListQuery(client, serviceName, promptMessage, list) { +-- +2.51.0 + + +From efb52899f505f1c54120298ac1cc7d3e476a4ff6 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= +Date: Wed, 28 Feb 2024 21:52:55 +0100 +Subject: [PATCH 08/13] js/gdm/util: Rename fingerprint device into devicePath + +It's just the dbus object path, so let's not be confused by that. + +Part-of: +--- + js/gdm/util.js | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/js/gdm/util.js b/js/gdm/util.js +index 24593f6b5c..2db9188070 100644 +--- a/js/gdm/util.js ++++ b/js/gdm/util.js +@@ -355,11 +355,11 @@ export class ShellUserVerifier extends Signals.EventEmitter { + return; + } + +- const [device] = await this._fprintManager.GetDefaultDeviceAsync( ++ const [devicePath] = await this._fprintManager.GetDefaultDeviceAsync( + Gio.DBusCallFlags.NONE, this._cancellable); + const fprintDeviceProxy = new FprintDeviceProxy(Gio.DBus.system, + 'net.reactivated.Fprint', +- device); ++ devicePath); + const fprintDeviceType = fprintDeviceProxy['scan-type']; + + this._fingerprintReaderType = fprintDeviceType === 'swipe' +-- +2.51.0 + + +From 885f1391ab85644377e6080be5ada5960ad209a5 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= +Date: Tue, 12 Apr 2022 16:51:21 +0200 +Subject: [PATCH 09/13] gdm/util: Only start fingerprint service synchronously + when it's default + +On ShellUserVerifier construction we used to start fprintd in a sync +fashion all the times, however in case the daemon had startup failures +or was hanging for whatever reason (like due to devices probing, given +that fprintd synchronously wait for them all to be initialized) we used +to just fail, leaving gdm or the lockscreen in a not usable state. + +While this could be prevented with a try/catch statement, there's no +much point to wait for fprintd if that's not the default authentication +service, and so: + - If we use gdm-fingerprint as default auth method, use a sync call to + initialize it and in case of failures, just continue with fallback + authentication mechanism (password) + + - Otherwise, asynchronously initialize fprintd and continue with the + ShellUserVerifier without fingerprint support until we got a reply. + In case the service fails to deliver us a result, we don't give up + but we will try doing that at each authentication via + _checkForFingerprintReader(). + In case all works properly, as per the previous commit, once the + initialization is done, we'll start the fingerprint PAM gdm service. + +Fixes #5168 + +Part-of: +--- + js/gdm/util.js | 100 +++++++++++++++++++++++++++++++++++++++---------- + 1 file changed, 80 insertions(+), 20 deletions(-) + +diff --git a/js/gdm/util.js b/js/gdm/util.js +index 79afbec..2a7644f 100644 +--- a/js/gdm/util.js ++++ b/js/gdm/util.js +@@ -146,6 +146,7 @@ var ShellUserVerifier = class { + + this._defaultService = null; + this._preemptingService = null; ++ this._fingerprintReaderType = FingerprintReaderType.NONE; + + this._messageQueue = []; + this._messageQueueTimeoutId = 0; +@@ -197,7 +198,8 @@ var ShellUserVerifier = class { + this._userName = userName; + this.reauthenticating = false; + +- this._checkForFingerprintReader().catch(logError); ++ this._checkForFingerprintReader().catch(e => ++ this._handleFingerprintError(e)); + + // If possible, reauthenticate an already running session, + // so any session specific credentials get updated appropriately +@@ -366,39 +368,85 @@ var ShellUserVerifier = class { + this.emit('show-message', null, null, MessageType.NONE); + } + +- _initFingerprintManager() { ++ async _initFingerprintManager() { + if (this._fprintManager) + return; + +- this._fprintManager = new Gio.DBusProxy({ ++ const fprintManager = new Gio.DBusProxy({ + g_connection: Gio.DBus.system, + g_name: 'net.reactivated.Fprint', + g_object_path: '/net/reactivated/Fprint/Manager', + g_interface_name: FprintManagerInfo.name, + g_interface_info: FprintManagerInfo, +- g_flags: Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES, ++ g_flags: Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES | ++ Gio.DBusProxyFlags.DO_NOT_AUTO_START_AT_CONSTRUCTION | ++ Gio.DBusProxyFlags.DO_NOT_CONNECT_SIGNALS, + }); +- this._fprintManager.init(null); ++ ++ try { ++ if (!this._getDetectedDefaultService()) { ++ // Other authentication methods would have already been detected by ++ // now as possibilities if they were available. ++ // If we're here it means that FINGERPRINT_AUTHENTICATION_KEY is ++ // true and so fingerprint authentication is our last potential ++ // option, so go ahead a synchronously look for a fingerprint device ++ // during startup or default service update. ++ fprintManager.init(null); ++ const [devicePath] = fprintManager.GetDefaultDeviceSync(); ++ this._fprintManager = fprintManager; ++ ++ const fprintDeviceProxy = new FprintDeviceProxy(Gio.DBus.system, ++ 'net.reactivated.Fprint', devicePath, null, null, ++ Gio.DBusProxyFlags.NOT_CONNECT_SIGNALS); ++ this._setFingerprintReaderType(fprintDeviceProxy['scan-type']); ++ } else { ++ // Ensure fingerprint service starts, but do not wait for it ++ const cancellable = this._cancellable; ++ await fprintManager.init_async(GLib.PRIORITY_DEFAULT, cancellable); ++ await this._updateFingerprintReaderType(fprintManager, cancellable); ++ this._fprintManager = fprintManager; ++ } ++ } catch (e) { ++ this._handleFingerprintError(e); ++ } + } + +- async _checkForFingerprintReader() { ++ _handleFingerprintError(e) { + this._fingerprintReaderType = FingerprintReaderType.NONE; + ++ 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) && ++ Gio.DBusError.get_remote_error(e) === ++ 'net.reactivated.Fprint.Error.NoSuchDevice') ++ return; ++ ++ logError(e, 'Failed to interact with fprintd service'); ++ } ++ ++ async _checkForFingerprintReader() { + if (!this._fprintManager) { + this._updateDefaultService(); + return; + } + +- const [devicePath] = await this._fprintManager.GetDefaultDeviceAsync( +- Gio.DBusCallFlags.NONE, this._cancellable); ++ if (this._fingerprintReaderType !== FingerprintReaderType.NONE) ++ return; ++ ++ await this._updateFingerprintReaderType(this._fprintManager, this._cancellable); ++ } ++ ++ async _updateFingerprintReaderType(fprintManager, cancellable) { ++ // Wrappers don't support null cancellable, so let's ignore it in case ++ const args = cancellable ? [cancellable] : []; ++ const [devicePath] = await fprintManager.GetDefaultDeviceAsync(...args); + const fprintDeviceProxy = new FprintDeviceProxy(Gio.DBus.system, + 'net.reactivated.Fprint', + devicePath); +- const fprintDeviceType = fprintDeviceProxy['scan-type']; +- +- this._fingerprintReaderType = fprintDeviceType === 'swipe' +- ? FingerprintReaderType.SWIPE +- : FingerprintReaderType.PRESS; ++ this._setFingerprintReaderType(fprintDeviceProxy['scan-type']); + this._updateDefaultService(); + + if (this._userVerifier && +@@ -409,6 +457,14 @@ var ShellUserVerifier = class { + } + } + ++ _setFingerprintReaderType(fprintDeviceType) { ++ this._fingerprintReaderType = ++ FingerprintReaderType[fprintDeviceType.toUpperCase()]; ++ ++ if (this._fingerprintReaderType === undefined) ++ throw new Error(`Unexpected fingerprint device type '${fprintDeviceType}'`); ++ } ++ + _onCredentialManagerAuthenticated(credentialManager, _token) { + this._preemptingService = credentialManager.service; + this.emit('credential-manager-authenticated'); +@@ -584,7 +640,7 @@ var ShellUserVerifier = class { + let needsReset = false; + + if (this._settings.get_boolean(FINGERPRINT_AUTHENTICATION_KEY)) { +- this._initFingerprintManager(); ++ this._initFingerprintManager().catch(logError); + } else if (this._fingerprintManager) { + this._fingerprintManager = null; + this._fingerprintReaderType = FingerprintReaderType.NONE; +@@ -608,17 +664,21 @@ var ShellUserVerifier = class { + this._cancelAndReset(); + } + +- _updateDefaultService() { +- const oldDefaultService = this._defaultService; +- ++ _getDetectedDefaultService() { + if (this._smartcardManager?.loggedInWithToken()) +- this._defaultService = SMARTCARD_SERVICE_NAME; ++ return SMARTCARD_SERVICE_NAME; + else if (this._settings.get_boolean(PASSWORD_AUTHENTICATION_KEY)) +- this._defaultService = PASSWORD_SERVICE_NAME; ++ return PASSWORD_SERVICE_NAME; + else if (this._smartcardManager) +- this._defaultService = SMARTCARD_SERVICE_NAME; ++ return SMARTCARD_SERVICE_NAME; + else if (this._fingerprintReaderType !== FingerprintReaderType.NONE) +- this._defaultService = FINGERPRINT_SERVICE_NAME; ++ return FINGERPRINT_SERVICE_NAME; ++ return null; ++ } ++ ++ _updateDefaultService() { ++ const oldDefaultService = this._defaultService; ++ this._defaultService = this._getDetectedDefaultService(); + + if (!this._defaultService) { + log("no authentication service is enabled, using password authentication"); +-- +2.51.0 + + +From ce03df5761df3c8571c18c0f291450ccb5ca37cc Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= +Date: Tue, 12 Apr 2022 17:20:58 +0200 +Subject: [PATCH 10/13] gdm/utils: Pass cancellable to the FPrint Device proxy + and avoid signals + +Part-of: +--- + js/gdm/util.js | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/js/gdm/util.js b/js/gdm/util.js +index e9ed77e0c9..7adeaebfa1 100644 +--- a/js/gdm/util.js ++++ b/js/gdm/util.js +@@ -410,8 +410,8 @@ export class ShellUserVerifier extends Signals.EventEmitter { + const args = cancellable ? [cancellable] : []; + const [devicePath] = await fprintManager.GetDefaultDeviceAsync(...args); + const fprintDeviceProxy = new FprintDeviceProxy(Gio.DBus.system, +- 'net.reactivated.Fprint', +- devicePath); ++ 'net.reactivated.Fprint', devicePath, null, cancellable, ++ Gio.DBusProxyFlags.NOT_CONNECT_SIGNALS); + this._setFingerprintReaderType(fprintDeviceProxy['scan-type']); + this._updateDefaultService(); + +-- +2.51.0 + + +From fa25156a6ce0765dfce5a4d5cfe25b3eaa2257f6 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= +Date: Tue, 12 Apr 2022 17:28:57 +0200 +Subject: [PATCH 11/13] gdm/util: Use fully async call to setup the fingerprint + device proxy + +Since fingerprint service can now be started also if a conversation has +already began, we can also initialize the proxy asynchronously, without +the risk that the service won't be started early enough. + +As per this, remove the usage of FprintDeviceProxy wrapper completely +since it's just not giving us anything here. + +Part-of: +--- + js/gdm/util.js | 25 +++++++++++++++++-------- + 1 file changed, 17 insertions(+), 8 deletions(-) + +diff --git a/js/gdm/util.js b/js/gdm/util.js +index eb81b34..a980918 100644 +--- a/js/gdm/util.js ++++ b/js/gdm/util.js +@@ -15,8 +15,8 @@ const SmartcardManager = imports.misc.smartcardManager; + + const FprintManagerInfo = Gio.DBusInterfaceInfo.new_for_xml( + loadInterfaceXML('net.reactivated.Fprint.Manager')); +-const FprintDeviceIface = loadInterfaceXML('net.reactivated.Fprint.Device'); +-const FprintDeviceProxy = Gio.DBusProxy.makeProxyWrapper(FprintDeviceIface); ++const FprintDeviceInfo = Gio.DBusInterfaceInfo.new_for_xml( ++ loadInterfaceXML('net.reactivated.Fprint.Device')); + + Gio._promisify(Gdm.Client.prototype, + 'open_reauthentication_channel', 'open_reauthentication_channel_finish'); +@@ -395,9 +395,8 @@ var ShellUserVerifier = class { + const [devicePath] = fprintManager.GetDefaultDeviceSync(); + this._fprintManager = fprintManager; + +- const fprintDeviceProxy = new FprintDeviceProxy(Gio.DBus.system, +- 'net.reactivated.Fprint', devicePath, null, null, +- Gio.DBusProxyFlags.NOT_CONNECT_SIGNALS); ++ const fprintDeviceProxy = this._getFingerprintDeviceProxy(devicePath); ++ fprintDeviceProxy.init(null); + this._setFingerprintReaderType(fprintDeviceProxy['scan-type']); + } else { + // Ensure fingerprint service starts, but do not wait for it +@@ -411,6 +410,17 @@ var ShellUserVerifier = class { + } + } + ++ _getFingerprintDeviceProxy(devicePath) { ++ return new Gio.DBusProxy({ ++ g_connection: Gio.DBus.system, ++ g_name: 'net.reactivated.Fprint', ++ g_object_path: devicePath, ++ g_interface_name: FprintDeviceInfo.name, ++ g_interface_info: FprintDeviceInfo, ++ g_flags: Gio.DBusProxyFlags.DO_NOT_CONNECT_SIGNALS, ++ }); ++ } ++ + _handleFingerprintError(e) { + this._fingerprintReaderType = FingerprintReaderType.NONE; + +@@ -443,9 +453,8 @@ var ShellUserVerifier = class { + // Wrappers don't support null cancellable, so let's ignore it in case + const args = cancellable ? [cancellable] : []; + const [devicePath] = await fprintManager.GetDefaultDeviceAsync(...args); +- const fprintDeviceProxy = new FprintDeviceProxy(Gio.DBus.system, +- 'net.reactivated.Fprint', devicePath, null, cancellable, +- Gio.DBusProxyFlags.NOT_CONNECT_SIGNALS); ++ const fprintDeviceProxy = this._getFingerprintDeviceProxy(devicePath); ++ await fprintDeviceProxy.init_async(GLib.PRIORITY_DEFAULT, cancellable); + this._setFingerprintReaderType(fprintDeviceProxy['scan-type']); + this._updateDefaultService(); + +-- +2.51.0 + + +From 7552875dbc78eb52e7d3b5a9f784c175ecca585c Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= +Date: Tue, 12 Apr 2022 17:05:02 +0200 +Subject: [PATCH 12/13] gdm/util: Reduce the fprintd proxy wait timeout + +Given that this may lead to the shell to hang on gdm startup, and that +we expect the service to be up and running quickly, we can safely set a +5 seconds timeout instead of using the longer GLib proxy defaults. + +Part-of: +--- + js/gdm/util.js | 5 +++++ + 1 file changed, 5 insertions(+) + +diff --git a/js/gdm/util.js b/js/gdm/util.js +index a980918..924bd2d 100644 +--- a/js/gdm/util.js ++++ b/js/gdm/util.js +@@ -46,6 +46,7 @@ var DISABLE_USER_LIST_KEY = 'disable-user-list'; + + // Give user 48ms to read each character of a PAM message + var USER_READ_TIME = 48; ++const FINGERPRINT_SERVICE_PROXY_TIMEOUT = 5000; + const FINGERPRINT_ERROR_TIMEOUT_WAIT = 15; + + var MessageType = { +@@ -392,6 +393,10 @@ var ShellUserVerifier = class { + // option, so go ahead a synchronously look for a fingerprint device + // during startup or default service update. + fprintManager.init(null); ++ // Do not wait too much for fprintd to reply, as in case it hangs ++ // we should fail early without having the shell to misbehave ++ fprintManager.set_default_timeout(FINGERPRINT_SERVICE_PROXY_TIMEOUT); ++ + const [devicePath] = fprintManager.GetDefaultDeviceSync(); + this._fprintManager = fprintManager; + +-- +2.51.0 + + +From 9075fc4733c62de18ab6c83e0aa32d26dd055569 Mon Sep 17 00:00:00 2001 +From: Joan Torres +Date: Mon, 9 Mar 2026 15:26:50 +0100 +Subject: [PATCH 13/13] gdm/util: Fix fprintManager DBus calls for older GJS + +The auto-generated GetDefaultDeviceSync/Async method wrappers don't +exist in this version. Use explicit call_sync/call methods instead. +--- + js/gdm/util.js | 18 ++++++++++++++---- + 1 file changed, 14 insertions(+), 4 deletions(-) + +diff --git a/js/gdm/util.js b/js/gdm/util.js +index 105d5d4..38a7015 100644 +--- a/js/gdm/util.js ++++ b/js/gdm/util.js +@@ -26,6 +26,8 @@ Gio._promisify(Gdm.UserVerifierProxy.prototype, + 'call_begin_verification_for_user', 'call_begin_verification_for_user_finish'); + Gio._promisify(Gdm.UserVerifierProxy.prototype, + 'call_begin_verification', 'call_begin_verification_finish'); ++Gio._promisify(Gio.DBusProxy.prototype, ++ 'call', 'call_finish'); + + var PASSWORD_SERVICE_NAME = 'gdm-password'; + var FINGERPRINT_SERVICE_NAME = 'gdm-fingerprint'; +@@ -397,7 +399,13 @@ var ShellUserVerifier = class { + // we should fail early without having the shell to misbehave + fprintManager.set_default_timeout(FINGERPRINT_SERVICE_PROXY_TIMEOUT); + +- const [devicePath] = fprintManager.GetDefaultDeviceSync(); ++ const result = fprintManager.call_sync( ++ 'GetDefaultDevice', ++ null, ++ Gio.DBusCallFlags.NONE, ++ FINGERPRINT_SERVICE_PROXY_TIMEOUT, ++ null); ++ const [devicePath] = result.deep_unpack(); + this._fprintManager = fprintManager; + + const fprintDeviceProxy = this._getFingerprintDeviceProxy(devicePath); +@@ -455,9 +463,13 @@ var ShellUserVerifier = class { + } + + async _updateFingerprintReaderType(fprintManager, cancellable) { +- // Wrappers don't support null cancellable, so let's ignore it in case +- const args = cancellable ? [cancellable] : []; +- const [devicePath] = await fprintManager.GetDefaultDeviceAsync(...args); ++ const result = await fprintManager.call( ++ 'GetDefaultDevice', ++ null, ++ Gio.DBusCallFlags.NONE, ++ -1, ++ cancellable); ++ const [devicePath] = result.deep_unpack(); + const fprintDeviceProxy = this._getFingerprintDeviceProxy(devicePath); + await fprintDeviceProxy.init_async(GLib.PRIORITY_DEFAULT, cancellable); + this._setFingerprintReaderType(fprintDeviceProxy['scan-type']); diff --git a/SOURCES/0001-loginManager-Update-RegisterSession.patch b/SOURCES/0001-loginManager-Update-RegisterSession.patch new file mode 100644 index 0000000..b5eadab --- /dev/null +++ b/SOURCES/0001-loginManager-Update-RegisterSession.patch @@ -0,0 +1,66 @@ +From 714332d3065a41e8bae61234cd231219cfcc95d6 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Tue, 3 Feb 2026 14:43:43 +0100 +Subject: [PATCH] main: Call RegisterDisplay once UI has initialized + +RegisterDisplay is called to signal that graphics have settled, +which allows GDM to terminate pending greeters and plymouth. + +Previously, RegisterSession was used for both recording the login +and terminating greeters. This caused a race condition where the +new user shell was setting up while the greeter was tearing down, +leading to configuration conflicts during the asynchronous handoff. + +By splitting into RegisterSession (called early to record login) and +RegisterDisplay (called after graphics settle), the greeter teardown +is delayed until the new session has completed its initial modeset. + +Fixes: 32a01330 ("main: Register session with GDM on startup") +Part-of: +--- + js/misc/loginManager.js | 18 ++++++++++++++++++ + js/ui/main.js | 2 ++ + 2 files changed, 20 insertions(+) + +diff --git a/js/misc/loginManager.js b/js/misc/loginManager.js +index 4eee98b..63caea3 100644 +--- a/js/misc/loginManager.js ++++ b/js/misc/loginManager.js +@@ -68,6 +68,24 @@ async function registerSessionWithGDM() { + } + } + ++async function registerDisplayWithGDM() { ++ log('Registering display with GDM'); ++ try { ++ await Gio.DBus.system.call( ++ 'org.gnome.DisplayManager', ++ '/org/gnome/DisplayManager/Manager', ++ 'org.gnome.DisplayManager.Manager', ++ 'RegisterDisplay', ++ GLib.Variant.new('(a{ss})', [{}]), null, ++ Gio.DBusCallFlags.NONE, -1, null); ++ } catch (e) { ++ if (!e.matches(Gio.DBusError, Gio.DBusError.UNKNOWN_METHOD)) ++ log(`Error registering display with GDM: ${e.message}`); ++ else ++ log('Not calling RegisterDisplay(): method not exported, GDM too old?'); ++ } ++} ++ + let _loginManager = null; + + /** +diff --git a/js/ui/main.js b/js/ui/main.js +index c753ab6..2bcc819 100644 +--- a/js/ui/main.js ++++ b/js/ui/main.js +@@ -309,6 +309,8 @@ function _initializeUI() { + sessionMode.currentMode !== 'initial-setup') + _handleLockScreenWarning(); + ++ LoginManager.registerDisplayWithGDM(); ++ + let perfModuleName = GLib.getenv("SHELL_PERF_MODULE"); + if (perfModuleName) { + let perfOutput = GLib.getenv("SHELL_PERF_OUTPUT"); diff --git a/SOURCES/enforce-smartcard-at-unlock.patch b/SOURCES/enforce-smartcard-at-unlock.patch index ffe2316..b4ade2c 100644 --- a/SOURCES/enforce-smartcard-at-unlock.patch +++ b/SOURCES/enforce-smartcard-at-unlock.patch @@ -74,7 +74,7 @@ index 72561daab..6b92e3564 100644 _updateDefaultService() { - if (this._settings.get_boolean(PASSWORD_AUTHENTICATION_KEY)) -+ if (this._smartcardManager.loggedInWithToken()) ++ if (this._smartcardManager?.loggedInWithToken()) + this._defaultService = SMARTCARD_SERVICE_NAME; + else if (this._settings.get_boolean(PASSWORD_AUTHENTICATION_KEY)) this._defaultService = PASSWORD_SERVICE_NAME; diff --git a/SOURCES/portal-notify.patch b/SOURCES/portal-notify.patch index 0b1028c..d2bba1d 100644 --- a/SOURCES/portal-notify.patch +++ b/SOURCES/portal-notify.patch @@ -332,7 +332,7 @@ diff --git a/data/icons/meson.build b/data/icons/meson.build index eff6e4b530..277df017b2 100644 --- a/data/icons/meson.build +++ b/data/icons/meson.build -@@ -1 +1,9 @@ +@@ -1,2 +1,11 @@ -install_subdir('hicolor', install_dir: icondir) +excluded_icons=[] +if not have_portal_helper @@ -343,6 +343,8 @@ index eff6e4b530..277df017b2 100644 +endif +install_subdir('hicolor', + install_dir: icondir, exclude_files: excluded_icons) ++ + install_subdir('scalable', install_dir: icondir) diff --git a/data/meson.build b/data/meson.build index 4a1e16d467..01cf828310 100644 --- a/data/meson.build diff --git a/SOURCES/post-changes-for-passwordless-gdm-backport.patch b/SOURCES/post-changes-for-passwordless-gdm-backport.patch new file mode 100644 index 0000000..d1b7103 --- /dev/null +++ b/SOURCES/post-changes-for-passwordless-gdm-backport.patch @@ -0,0 +1,2866 @@ +From 183d2cce2a34a0d6fca990c2264bae60cdfc5de4 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Wed, 25 Mar 2026 18:58:29 +0100 +Subject: [PATCH 01/30] gdm: Update styles from login-lock to login-dialog + +login-lock is used in newer versions, but in this version login-dialog +is used. +--- + data/theme/gnome-shell-sass/_widgets.scss | 1 + + .../widgets/_login-dialog.scss | 307 +++++++++++++++++- + .../gnome-shell-sass/widgets/_qr-code.scss | 4 +- + 3 files changed, 296 insertions(+), 16 deletions(-) + +diff --git a/data/theme/gnome-shell-sass/_widgets.scss b/data/theme/gnome-shell-sass/_widgets.scss +index a8d0aa9f4d..1651d9ca3b 100644 +--- a/data/theme/gnome-shell-sass/_widgets.scss ++++ b/data/theme/gnome-shell-sass/_widgets.scss +@@ -46,6 +46,7 @@ + @import 'widgets/tiled-previews'; + @import 'widgets/keyboard'; + @import 'widgets/looking-glass'; ++@import 'widgets/qr-code'; + // Lock / login screens + @import 'widgets/login-dialog'; + @import 'widgets/screen-shield'; +diff --git a/data/theme/gnome-shell-sass/widgets/_login-dialog.scss b/data/theme/gnome-shell-sass/widgets/_login-dialog.scss +index 307a751628..f3aeed4bf3 100644 +--- a/data/theme/gnome-shell-sass/widgets/_login-dialog.scss ++++ b/data/theme/gnome-shell-sass/widgets/_login-dialog.scss +@@ -1,5 +1,7 @@ + /* Login Dialog */ + ++$_gdm_fg: $osd_fg_color; ++ + .login-dialog-banner-view { + padding-top: 24px; + max-width: 23em; +@@ -70,8 +72,10 @@ + } + } + ++ .next-button, + .cancel-button, + .switch-user-button, ++ .login-dialog-auth-menu-button, + .login-dialog-session-list-button { + padding: 0; + border-radius: 99px; +@@ -83,10 +87,40 @@ + StIcon { icon-size: $base_icon_size; } + } + ++ .next-button { ++ background-color: transparent !important; ++ } ++ ++ .switch-user-button { ++ min-width: 8.5em; ++ min-height: 2.5em; ++ @include fontsize($base_font_size - 1); ++ font-weight: bold; ++ } ++ + .caps-lock-warning-label, + .login-dialog-message-warning { + color: $osd_fg_color; + } ++ ++ .login-dialog-prompt-layout { ++ &.web-login-active { ++ width: 23em * 1.5; ++ } ++ } ++ ++ .login-dialog-prompt-entry-area { ++ margin: 0.5em $base_margin * 5; ++ } ++ ++ .login-dialog-prompt-entry { ++ border-radius: $base_border_radius * 1.5; ++ padding-right: 2.5em; // Make room for button-well inside entry ++ } ++ ++ .login-dialog-default-button-well { ++ margin-right: 0.4em; ++ } + } + + .login-dialog-bottom-button-group { +@@ -118,30 +152,185 @@ + padding-top: 1em; + } + +-.login-dialog-auth-list-view { -st-vfade-offset: 1em; } ++.login-dialog-auth-menu-button-popup { ++ padding: $base_padding * 3; ++ margin-right: $base_padding * 2; ++ ++ .login-dialog-auth-menu-item-section { ++ padding-top: $base_padding; ++ ++ &:first-child { ++ padding-top: 0; ++ } ++ ++ &-label { ++ @include fontsize($base_font_size - 1); ++ font-weight: bold; ++ color: $_gdm_fg; ++ } ++ } ++ ++ .login-dialog-auth-menu-item-icon { ++ color: $_gdm_fg; ++ icon-size: $base_icon_size; ++ margin-right: $base_margin * 2; ++ } ++ ++ .login-dialog-auth-menu-item-box { ++ spacing: $base_padding * .5; ++ ++ &-name { ++ color: $_gdm_fg; ++ @include fontsize($base_font_size); ++ font-weight: bold; ++ } ++ ++ &-description { ++ color: $_gdm_fg; ++ @include fontsize($base_font_size - 1); ++ } ++ } ++} ++ ++.login-dialog-auth-menu-button-indicator { ++ background-color: transparent !important; ++ border-width: 0; ++ margin-bottom: 32px; ++ margin-left: 32px; ++ ++ .login-dialog-auth-menu-button-indicator-icons { ++ spacing: $base_padding * 3; ++ ++ .login-dialog-auth-menu-button-indicator-icon { ++ icon-size: 2em; ++ } ++ } ++ ++ .login-dialog-auth-menu-button-indicator-description { ++ @include fontsize($base_font_size); ++ margin-left: $base_padding * 2; ++ min-width: 20em; ++ } ++} ++ ++.login-dialog-button-box { ++ height: 4em; ++} ++ ++.login-dialog-auth-list-view { ++ -st-vfade-offset: 1em; ++ max-height: 13em; ++} ++ + .login-dialog-auth-list { +- spacing: 6px; +- margin-left: 2em; ++ spacing: $base_padding * 1.5; ++ width: 23em; ++ margin-left: $base_margin * 5; + } + +-.login-dialog-auth-list-title { +- margin-left: 2em; ++.login-dialog { ++ .login-dialog-auth-list-title, ++ .login-dialog-auth-list-item { ++ border-radius: $base_border_radius * 1.5; ++ color: darken($_gdm_fg, 30%); ++ &:focus, &:selected { background-color: $selected_bg_color; color: $selected_fg_color; } ++ } ++ ++ .login-dialog-auth-list-title { ++ background-color: transparentize($_gdm_fg, .97) !important; ++ color: $_gdm_fg !important; ++ padding: $base_padding; ++ margin: 0.5em $base_margin * 5; ++ } ++ ++ .login-dialog-auth-list-item { ++ min-height: 3em; ++ padding: $base_padding * 1.5; ++ margin-bottom: $base_margin; ++ background-color: transparentize($_gdm_fg, .94); ++ } + } + +-.login-dialog-auth-list-item { +- border-radius: $base_border_radius + 4px; +- padding: 6px; +- color: darken($osd_fg_color,30%); +- &:focus, &:selected { background-color: $selected_bg_color; color: $selected_fg_color; } ++.unlock-dialog { ++ .login-dialog-auth-list-item { ++ border: none; ++ color: $osd_fg_color; ++ background-color: transparentize($osd_fg_color, 0.94); ++ &:hover, &:focus, &:selected { background-color: transparentize($osd_fg_color, 0.84); } ++ &:active { background-color: transparentize($osd_fg_color, 0.78); } ++ border-radius: $base_border_radius * 1.5; ++ min-height: 3em; ++ padding: $base_padding * 1.5; ++ margin-bottom: $base_margin; ++ } ++ ++ .login-dialog-auth-list-title { ++ background-color: transparent !important; ++ border: none; ++ color: $osd_fg_color !important; ++ padding: $base_padding; ++ } + } + +-.login-dialog-auth-list-label { ++.login-dialog-auth-list-title-label { + @include fontsize($base_font_size + 2); + font-weight: bold; +- padding-left: 15px; ++ padding: $base_padding; ++ text-align: center; ++} + +- &:ltr { padding-left: 14px; text-align: left; } +- &:rtl { padding-right: 14px; text-align: right; } ++.login-dialog-auth-list-item-title, ++.login-dialog-auth-list-item-subtitle { ++ @include fontsize($base_font_size); ++ text-align: center; ++ padding: $base_padding * 0.3 0; ++} ++ ++.login-dialog-auth-list-item-title { ++ color: $_gdm_fg; ++ font-weight: bold; ++} ++ ++.login-dialog-auth-list-item-subtitle { ++ color: darken($_gdm_fg, 20%); ++ font-weight: 500; ++} ++ ++.login-dialog-item-icon { ++ width: 1.3em; ++ height: 1.3em; ++ color: $_gdm_fg; ++ padding: $base_padding * 0.7 $base_padding; ++ border-radius: $base_border_radius; ++ ++ &:hover { ++ background-color: $system_bg_color; ++ } ++ ++ &-popup { ++ &.popup-menu { ++ min-width: 0; ++ } ++ ++ &-box { ++ padding: $base_padding 0; ++ } ++ ++ &-box &-labels { ++ spacing: $base_padding * 0.5; ++ font-weight: bold; ++ text-align: center; ++ ++ & > :first-child { ++ color: darken($fg_color, 30%); ++ font-weight: 500; ++ } ++ ++ & > :last-child { ++ color: $fg_color; ++ } ++ } ++ } + } + + .login-dialog-user-list-view { -st-vfade-offset: 1em; } +@@ -207,3 +396,93 @@ + @include fontsize($base_font_size + 1); + padding-top: 1em; + } ++ ++.web-login-spinner { ++ background-color: rgba(0, 0, 0, 0.5); ++ border: 5px rgba(0, 0, 0, 0.0); ++ border-radius: 50px; ++} ++ ++.web-login-title-label { ++ @include fontsize($base_font_size); ++ color: darken($_gdm_fg,30%); ++ text-align: center; ++} ++ ++.web-login-url-label { ++ @include fontsize($base_font_size); ++ font-family: monospace; ++ color: $_gdm_fg; ++ text-align: center; ++ ++ &.web-login-url-label-long { ++ @include fontsize($base_font_size - 2); ++ } ++} ++ ++.web-login-code-title-label { ++ @include fontsize($base_font_size); ++ color: $_gdm_fg; ++ text-align: center; ++} ++ ++.web-login-code-label { ++ @include fontsize($base_font_size); ++ color: $_gdm_fg; ++ font-weight: bold; ++ text-align: center; ++} ++ ++.web-login-prompt { ++ padding-top: $base_padding; ++ padding-bottom: $base_padding; ++ padding-left: $base_padding * 4.5; ++ padding-right: $base_padding * 4.5; ++ spacing: 1.75em; ++} ++ ++.web-login-button-label { ++ @include fontsize($base_font_size + 2); ++ color: $_gdm_fg; ++ min-width: 12em; ++ text-align: center; ++ font-weight: bold; ++ padding-bottom: 0.1em; ++} ++ ++.login-dialog, ++.unlock-dialog { ++ .web-login-intro-button, ++ .web-login-prompt-button { ++ border-radius: $base_border_radius * 4 !important; ++ padding: $base_padding * 1.5 $base_padding * 3; ++ background-color: transparentize($_gdm_fg, .94); ++ } ++ ++ .web-login-intro-button { ++ width: 10em; ++ } ++ ++ .web-login-prompt-button { ++ width: 7em; ++ margin: $base_margin * 6 $base_margin * 2; ++ } ++} ++ ++.login-dialog { ++ .web-login-intro-button, ++ .web-login-prompt-button { ++ &:hover, &:focus { ++ background-color: $selected_bg_color; ++ } ++ } ++} ++ ++.unlock-dialog { ++ .web-login-intro-button, ++ .web-login-prompt-button { ++ &:hover, &:focus { ++ background-color: transparentize($osd_fg_color, .84); ++ } ++ } ++} +diff --git a/data/theme/gnome-shell-sass/widgets/_qr-code.scss b/data/theme/gnome-shell-sass/widgets/_qr-code.scss +index da04425bd6..0d733453f2 100644 +--- a/data/theme/gnome-shell-sass/widgets/_qr-code.scss ++++ b/data/theme/gnome-shell-sass/widgets/_qr-code.scss +@@ -7,8 +7,8 @@ + background-color: $qrcode_bg_color; + border-color: $qrcode_bg_color; + } @else { +- background-color: $system_fg_color; +- border-color: $system_fg_color; ++ background-color: $osd_fg_color; ++ border-color: $osd_fg_color; + color: $system_bg_color; + } + } +-- +2.51.0 + + +From a6a9baab3e92e3adb9172227051e83c4d145e1df Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Fri, 26 Dec 2025 14:52:58 +0100 +Subject: [PATCH 02/30] gdm: Fix importing using the old gjs style + +--- + js/gdm/authMenuButton.js | 13 +++++-------- + js/gdm/authServices.js | 18 +++++++----------- + js/gdm/authServicesLegacy.js | 18 ++++++++---------- + js/gdm/authServicesSSSDSwitchable.js | 10 ++++------ + js/gdm/webLogin.js | 14 +++++++------- + js/misc/fingerprintManager.js | 10 ++++------ + js/misc/passkeyDeviceManager.js | 3 +-- + js/ui/qrCode.js | 10 +++------- + 8 files changed, 39 insertions(+), 57 deletions(-) + +diff --git a/js/gdm/authMenuButton.js b/js/gdm/authMenuButton.js +index 9b7cebf6d8..f7f458dec8 100644 +--- a/js/gdm/authMenuButton.js ++++ b/js/gdm/authMenuButton.js +@@ -1,4 +1,5 @@ + // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- ++/* exported AuthMenuButton, AuthMenuButtonIndicator */ + /* + * Copyright 2024 Red Hat, Inc + * +@@ -43,15 +44,11 @@ + * deactivates any other active item in the same section. + */ + +-import Atk from 'gi://Atk'; +-import Clutter from 'gi://Clutter'; +-import GObject from 'gi://GObject'; +-import Shell from 'gi://Shell'; +-import St from 'gi://St'; ++const { Atk, Clutter, GObject, Shell, St } = imports.gi; + +-import * as BoxPointer from '../ui/boxpointer.js'; +-import * as Main from '../ui/main.js'; +-import * as PopupMenu from '../ui/popupMenu.js'; ++const BoxPointer = imports.ui.boxpointer; ++const Main = imports.ui.main; ++const PopupMenu = imports.ui.popupMenu; + + const VISIBILITY_ANIMATION_TIME = 200; + +diff --git a/js/gdm/authServices.js b/js/gdm/authServices.js +index 0a94f493a2..0cafd2831c 100644 +--- a/js/gdm/authServices.js ++++ b/js/gdm/authServices.js +@@ -1,16 +1,12 @@ + // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +-import * as FingerprintManager from '../misc/fingerprintManager.js'; +-import * as Params from '../misc/params.js'; +-import {registerDestroyableType} from '../misc/signalTracker.js'; +-import * as PasskeyDeviceManager from '../misc/passkeyDeviceManager.js'; +-import * as SmartcardManager from '../misc/smartcardManager.js'; +-import {logErrorUnlessCancelled} from '../misc/errorUtils.js'; +-import * as Util from './util.js'; +-import Gdm from 'gi://Gdm'; +-import GLib from 'gi://GLib'; +-import Gio from 'gi://Gio'; +-import GObject from 'gi://GObject'; ++const { Gdm, Gio, GLib, GObject } = imports.gi; ++ ++const FingerprintManager = imports.misc.fingerprintManager; ++const Params = imports.misc.params; ++const PasskeyDeviceManager = imports.misc.passkeyDeviceManager; ++const SmartcardManager = imports.misc.smartcardManager; ++const Util = imports.gdm.util; + + Gio._promisify(Gdm.Client.prototype, 'open_reauthentication_channel'); + Gio._promisify(Gdm.Client.prototype, 'get_user_verifier'); +diff --git a/js/gdm/authServicesLegacy.js b/js/gdm/authServicesLegacy.js +index c1ff1242dc..dd134db4b9 100644 +--- a/js/gdm/authServicesLegacy.js ++++ b/js/gdm/authServicesLegacy.js +@@ -1,13 +1,11 @@ +-import GLib from 'gi://GLib'; +-import GObject from 'gi://GObject'; +- +-import * as Constants from './constants.js'; +-import {FingerprintReaderType} from '../misc/fingerprintManager.js'; +-import {logErrorUnlessCancelled} from '../misc/errorUtils.js'; +-import * as OVirt from './oVirt.js'; +-import * as Util from './util.js'; +-import * as Vmware from './vmware.js'; +-import {AuthServices} from './authServices.js'; ++const { GLib, GObject } = imports.gi; ++ ++const {AuthServices} = imports.gdm.authServices; ++const Constants = imports.gdm.constants; ++const {FingerprintReaderType} = imports.misc.fingerprintManager; ++const OVirt = imports.gdm.oVirt; ++const Util = imports.gdm.util; ++const Vmware = imports.gdm.vmware; + + const FINGERPRINT_ERROR_TIMEOUT_WAIT = 15; + const FINGERPRINT_READY_TIMEOUT_MS = 500; +diff --git a/js/gdm/authServicesSSSDSwitchable.js b/js/gdm/authServicesSSSDSwitchable.js +index de03dec1d7..4f74a2040d 100644 +--- a/js/gdm/authServicesSSSDSwitchable.js ++++ b/js/gdm/authServicesSSSDSwitchable.js +@@ -1,10 +1,8 @@ +-import GLib from 'gi://GLib'; +-import GObject from 'gi://GObject'; ++const { GLib, GObject } = imports.gi; + +-import * as Constants from './constants.js'; +-import {logErrorUnlessCancelled} from '../misc/errorUtils.js'; +-import * as Util from './util.js'; +-import {AuthServices} from './authServices.js'; ++const {AuthServices} = imports.gdm.authServices; ++const Constants = imports.gdm.constants; ++const Util = imports.gdm.util; + + const MechanismsStatus = { + WAITING: 0, +diff --git a/js/gdm/webLogin.js b/js/gdm/webLogin.js +index 8f2a0d8767..ef4f28b592 100644 +--- a/js/gdm/webLogin.js ++++ b/js/gdm/webLogin.js +@@ -3,13 +3,13 @@ + // A widget showing a URL for web login + /* exported WebLoginPrompt */ + +-import Clutter from 'gi://Clutter'; +-import GObject from 'gi://GObject'; +-import Pango from 'gi://Pango'; +-import St from 'gi://St'; +-import {Spinner} from '../ui/animation.js'; +-import * as Params from '../misc/params.js'; +-import {QrCode} from '../ui/qrCode.js'; ++const { Clutter, Gio, GnomeQR, GObject, Pango, St } = imports.gi; ++ ++const Params = imports.misc.params; ++const { Spinner } = imports.ui.animation; ++const { QrCode } = imports.ui.qrCode; ++ ++Gio._promisify(GnomeQR, 'generate_qr_code_async'); + + const QR_CODE_SIZE = 150; + const WEB_LOGIN_SPINNER_SIZE = 35; +diff --git a/js/misc/fingerprintManager.js b/js/misc/fingerprintManager.js +index 6ef2d68d3a..cdeb3579d1 100644 +--- a/js/misc/fingerprintManager.js ++++ b/js/misc/fingerprintManager.js +@@ -1,13 +1,11 @@ +-import Gio from 'gi://Gio'; +-import GLib from 'gi://GLib'; +-import GObject from 'gi://GObject'; ++const { Gio, GLib, GObject } = imports.gi; + +-import {loadInterfaceXML} from './fileUtils.js'; ++const FileUtils = imports.misc.fileUtils; + + const FprintManagerInfo = Gio.DBusInterfaceInfo.new_for_xml( +- loadInterfaceXML('net.reactivated.Fprint.Manager')); ++ FileUtils.loadInterfaceXML('net.reactivated.Fprint.Manager')); + const FprintDeviceInfo = Gio.DBusInterfaceInfo.new_for_xml( +- loadInterfaceXML('net.reactivated.Fprint.Device')); ++ FileUtils.loadInterfaceXML('net.reactivated.Fprint.Device')); + + export const FingerprintReaderType = { + NONE: 0, +diff --git a/js/misc/passkeyDeviceManager.js b/js/misc/passkeyDeviceManager.js +index 20799975aa..2aa81d2ce6 100644 +--- a/js/misc/passkeyDeviceManager.js ++++ b/js/misc/passkeyDeviceManager.js +@@ -1,5 +1,4 @@ +-import GObject from 'gi://GObject'; +-import GUdev from 'gi://GUdev'; ++const { GObject, GUdev } = imports.gi; + + let _passkeyDeviceManager = null; + +diff --git a/js/ui/qrCode.js b/js/ui/qrCode.js +index caf0999f34..d1fac46847 100644 +--- a/js/ui/qrCode.js ++++ b/js/ui/qrCode.js +@@ -1,10 +1,6 @@ +-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'; ++/* exported QrCode */ ++ ++const { Clutter, Cogl, Gio, GnomeQR, GObject, St } = imports.gi; + + Gio._promisify(GnomeQR, 'generate_qr_code_async'); + +-- +2.51.0 + + +From 12ea582a78b26f37c9c7bdd12af35718cbbae961 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Fri, 26 Dec 2025 14:58:42 +0100 +Subject: [PATCH 03/30] gdm: Fix using the old gjs style for classes and + functions + +--- + js/gdm/authList.js | 45 +++++++++------------- + js/gdm/authMenuButton.js | 57 +++++++++++----------------- + js/gdm/authServices.js | 20 ++++------ + js/gdm/authServicesLegacy.js | 15 ++++---- + js/gdm/authServicesSSSDSwitchable.js | 15 ++++---- + js/gdm/constants.js | 22 ++++++----- + js/gdm/util.js | 9 +++-- + js/gdm/webLogin.js | 20 +++++----- + js/misc/fingerprintManager.js | 30 +++++++-------- + js/misc/passkeyDeviceManager.js | 23 +++++------ + js/ui/qrCode.js | 40 +++++++++---------- + 11 files changed, 131 insertions(+), 165 deletions(-) + +diff --git a/js/gdm/authList.js b/js/gdm/authList.js +index 3f1b2817f9..6eb34f6d97 100644 +--- a/js/gdm/authList.js ++++ b/js/gdm/authList.js +@@ -57,13 +57,10 @@ const ItemIconPopup = class extends PopupMenu.PopupMenu { + } + }; + ++const ItemIcon = GObject.registerClass( + class ItemIcon extends St.Button { +- static { +- GObject.registerClass(this); +- } +- +- constructor(iconName, iconTitle, iconSubtitle) { +- super({ ++ _init(iconName, iconTitle, iconSubtitle) { ++ super._init({ + style_class: 'login-dialog-item-icon', + iconName, + }); +@@ -71,21 +68,17 @@ class ItemIcon extends St.Button { + this._popup = new ItemIconPopup(this, iconTitle, iconSubtitle); + Main.uiGroup.add_child(this._popup.actor); + } +-} ++}); + +-class AuthListItem extends St.Button { +- static [GObject.signals] = { ++const AuthListItem = GObject.registerClass({ ++ Signals: { + 'activate': {}, +- }; +- +- static { +- GObject.registerClass(this); +- } +- +- constructor(key, content) { ++ }, ++}, class AuthListItem extends St.Button { ++ _init(key, content) { + const {title, subtitle, iconName, iconTitle, iconSubtitle} = content; + +- super({ ++ super._init({ + style_class: 'login-dialog-auth-list-item', + button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE, + can_focus: true, +@@ -168,20 +161,16 @@ class AuthListItem extends St.Button { + this.remove_style_pseudo_class('selected'); + } + } +-} ++}); + +-export class AuthList extends St.BoxLayout { +- static [GObject.signals] = { ++var AuthList = GObject.registerClass({ ++ Signals: { + 'activate': {param_types: [GObject.TYPE_STRING]}, + 'item-added': {param_types: [AuthListItem.$gtype]}, +- }; +- +- static { +- GObject.registerClass(this); + } +- +- constructor() { +- super({ ++}, class AuthList extends St.BoxLayout { ++ _init() { ++ super._init({ + orientation: Clutter.Orientation.VERTICAL, + style_class: 'login-dialog-auth-list-layout', + y_align: Clutter.ActorAlign.CENTER, +@@ -278,4 +267,4 @@ export class AuthList extends St.BoxLayout { + this._box.destroy_all_children(); + this._items.clear(); + } +-} ++}); +diff --git a/js/gdm/authMenuButton.js b/js/gdm/authMenuButton.js +index f7f458dec8..e58c331b45 100644 +--- a/js/gdm/authMenuButton.js ++++ b/js/gdm/authMenuButton.js +@@ -71,13 +71,10 @@ class AuthMenuItemSection extends PopupMenu.PopupMenuSection { + } + } + ++const AuthMenuItem = GObject.registerClass( + class AuthMenuItem extends PopupMenu.PopupBaseMenuItem { +- static { +- GObject.registerClass(this); +- } +- +- constructor(item, params = {}) { +- super(params); ++ _init(item, params = {}) { ++ super._init(params); + + if (item.description && item.iconName) { + const icon = new St.Icon({ +@@ -117,42 +114,37 @@ class AuthMenuItem extends PopupMenu.PopupBaseMenuItem { + + this.setOrnament(PopupMenu.Ornament.HIDDEN); + } +-} ++}); + +-export class AuthMenuButton extends St.Button { +- static [GObject.properties] = { ++var AuthMenuButton = GObject.registerClass({ ++ Properties: { + 'title': GObject.ParamSpec.string( +- 'title', null, null, ++ 'title', 'title', 'title', + GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY, + ''), + 'icon-name': GObject.ParamSpec.string( +- 'icon-name', null, null, ++ 'icon-name', 'icon-name', 'icon-name', + GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY, + ''), + 'read-only': GObject.ParamSpec.boolean( +- 'read-only', null, null, ++ 'read-only', 'read-only', 'read-only', + GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY, + false), + 'section-order': GObject.ParamSpec.jsobject( +- 'section-order', null, null, ++ 'section-order', 'section-order', 'section-order', + GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY), + 'animate-visibility': GObject.ParamSpec.boolean( +- 'animate-visibility', null, null, ++ 'animate-visibility', 'animate-visibility', 'animate-visibility', + GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY, + false), +- }; +- +- static [GObject.signals] = { ++ }, ++ Signals: { + 'active-item-changed': {param_types: [GObject.TYPE_STRING]}, +- }; +- +- static { +- GObject.registerClass(this); +- } +- +- constructor(params) { ++ }, ++}, class AuthMenuButton extends St.Button { ++ _init(params) { + params.sectionOrder ??= []; +- super({ ++ super._init({ + ...params, + style_class: 'login-dialog-button login-dialog-auth-menu-button', + reactive: true, +@@ -452,16 +444,13 @@ export class AuthMenuButton extends St.Button { + closeMenu() { + this._menu.close(); + } +-} ++}); + +-export class AuthMenuButtonIndicator extends AuthMenuButton { +- static { +- GObject.registerClass(this); +- } +- +- constructor(params = {}) { ++var AuthMenuButtonIndicator = GObject.registerClass( ++class AuthMenuButtonIndicator extends AuthMenuButton { ++ _init(params = {}) { + params.readOnly ??= true; +- super(params); ++ super._init(params); + + this.add_style_class_name('login-dialog-auth-menu-button-indicator'); + } +@@ -526,4 +515,4 @@ export class AuthMenuButtonIndicator extends AuthMenuButton { + // Override to force visibility even when there's only one item + _updateVisibility() { + } +-} ++}); +diff --git a/js/gdm/authServices.js b/js/gdm/authServices.js +index 0cafd2831c..ce4f0729b3 100644 +--- a/js/gdm/authServices.js ++++ b/js/gdm/authServices.js +@@ -1,4 +1,5 @@ + // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- ++/* exported AuthServices */ + + const { Gdm, Gio, GLib, GObject } = imports.gi; + +@@ -16,8 +17,8 @@ Gio._promisify(Gdm.UserVerifierProxy.prototype, 'call_answer_query'); + Gio._promisify(Gdm.UserVerifierChoiceListProxy.prototype, 'call_select_choice'); + Gio._promisify(Gdm.UserVerifierCustomJSONProxy.prototype, 'call_reply'); + +-export class AuthServices extends GObject.Object { +- static [GObject.signals] = { ++var AuthServices = GObject.registerClass({ ++ Signals: { + 'destroy': {}, + 'queue-message': { + param_types: [GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_UINT], +@@ -51,13 +52,8 @@ export class AuthServices extends GObject.Object { + GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_JSOBJECT, + ], + }, +- }; +- +- static { +- GObject.registerClass(this); +- registerDestroyableType(this); +- } +- ++ }, ++}, class AuthServices extends GObject.Object { + static SupportedRoles = []; + static RoleToService = {}; + +@@ -65,8 +61,8 @@ export class AuthServices extends GObject.Object { + return roles.some(r => this.SupportedRoles.includes(r)); + } + +- constructor(params) { +- super(); ++ _init(params) { ++ super._init(); + params = Params.parse(params, { + client: null, + enabledRoles: [], +@@ -502,4 +498,4 @@ export class AuthServices extends GObject.Object { + throw new GObject.NotImplementedError( + `_handleCanStartService in ${this.constructor.name}`); + } +-} ++}); +diff --git a/js/gdm/authServicesLegacy.js b/js/gdm/authServicesLegacy.js +index dd134db4b9..8922546474 100644 +--- a/js/gdm/authServicesLegacy.js ++++ b/js/gdm/authServicesLegacy.js +@@ -1,3 +1,5 @@ ++/* exported AuthServicesLegacy */ ++ + const { GLib, GObject } = imports.gi; + + const {AuthServices} = imports.gdm.authServices; +@@ -28,7 +30,8 @@ const Mechanisms = [ + }, + ]; + +-export class AuthServicesLegacy extends AuthServices { ++var AuthServicesLegacy = GObject.registerClass({ ++}, class AuthServicesLegacy extends AuthServices { + static SupportedRoles = [ + Constants.PASSWORD_ROLE_NAME, + Constants.SMARTCARD_ROLE_NAME, +@@ -41,12 +44,8 @@ export class AuthServicesLegacy extends AuthServices { + [Constants.FINGERPRINT_ROLE_NAME]: Constants.FINGERPRINT_SERVICE_NAME, + }; + +- static { +- GObject.registerClass(this); +- } +- +- constructor(params) { +- super(params); ++ _init(params) { ++ super._init(params); + + this._updateEnabledMechanisms(); + +@@ -378,4 +377,4 @@ export class AuthServicesLegacy extends AuthServices { + }; + this.emit('reset', {softReset: true}); + } +-} ++}); +diff --git a/js/gdm/authServicesSSSDSwitchable.js b/js/gdm/authServicesSSSDSwitchable.js +index 4f74a2040d..4b4761d105 100644 +--- a/js/gdm/authServicesSSSDSwitchable.js ++++ b/js/gdm/authServicesSSSDSwitchable.js +@@ -1,3 +1,5 @@ ++/* exported AuthServicesSwitchable */ ++ + const { GLib, GObject } = imports.gi; + + const {AuthServices} = imports.gdm.authServices; +@@ -10,7 +12,8 @@ const MechanismsStatus = { + FOUND: 2, + }; + +-export class AuthServicesSSSDSwitchable extends AuthServices { ++var AuthServicesSSSDSwitchable = GObject.registerClass({ ++}, class AuthServicesSSSDSwitchable extends AuthServices { + static SupportedRoles = [ + Constants.PASSWORD_ROLE_NAME, + Constants.SMARTCARD_ROLE_NAME, +@@ -25,12 +28,8 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + [Constants.WEB_LOGIN_ROLE_NAME]: Constants.SWITCHABLE_AUTH_SERVICE_NAME, + }; + +- static { +- GObject.registerClass(this); +- } +- +- constructor(params) { +- super(params); ++ _init(params) { ++ super._init(params); + + this._mechanismsStatus = MechanismsStatus.WAITING; + } +@@ -440,4 +439,4 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + GLib.source_remove(this._webLoginTimeoutId); + this._webLoginTimeoutId = 0; + } +-} ++}); +diff --git a/js/gdm/constants.js b/js/gdm/constants.js +index 7c48599e64..cfbbe32426 100644 +--- a/js/gdm/constants.js ++++ b/js/gdm/constants.js +@@ -1,12 +1,16 @@ + // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- ++/* exported PASSWORD_ROLE_NAME, SMARTCARD_ROLE_NAME, FINGERPRINT_ROLE_NAME, ++ PASSKEY_ROLE_NAME, WEB_LOGIN_ROLE_NAME, PASSWORD_SERVICE_NAME, ++ SMARTCARD_SERVICE_NAME, FINGERPRINT_SERVICE_NAME, ++ SWITCHABLE_AUTH_SERVICE_NAME */ + +-export const PASSWORD_ROLE_NAME = 'password'; +-export const SMARTCARD_ROLE_NAME = 'smartcard'; +-export const FINGERPRINT_ROLE_NAME = 'fingerprint'; +-export const PASSKEY_ROLE_NAME = 'passkey'; +-export const WEB_LOGIN_ROLE_NAME = 'eidp'; ++var PASSWORD_ROLE_NAME = 'password'; ++var SMARTCARD_ROLE_NAME = 'smartcard'; ++var FINGERPRINT_ROLE_NAME = 'fingerprint'; ++var PASSKEY_ROLE_NAME = 'passkey'; ++var WEB_LOGIN_ROLE_NAME = 'eidp'; + +-export const PASSWORD_SERVICE_NAME = 'gdm-password'; +-export const SMARTCARD_SERVICE_NAME = 'gdm-smartcard'; +-export const FINGERPRINT_SERVICE_NAME = 'gdm-fingerprint'; +-export const SWITCHABLE_AUTH_SERVICE_NAME = 'gdm-switchable-auth'; ++var PASSWORD_SERVICE_NAME = 'gdm-password'; ++var SMARTCARD_SERVICE_NAME = 'gdm-smartcard'; ++var FINGERPRINT_SERVICE_NAME = 'gdm-fingerprint'; ++var SWITCHABLE_AUTH_SERVICE_NAME = 'gdm-switchable-auth'; +diff --git a/js/gdm/util.js b/js/gdm/util.js +index 743fce11b7..f1709edea3 100644 +--- a/js/gdm/util.js ++++ b/js/gdm/util.js +@@ -1,6 +1,7 @@ + // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + /* exported BANNER_MESSAGE_KEY, BANNER_MESSAGE_TEXT_KEY, LOGO_KEY, +- DISABLE_USER_LIST_KEY, fadeInActor, fadeOutActor, cloneAndFadeOutActor */ ++ DISABLE_USER_LIST_KEY, fadeInActor, fadeOutActor, cloneAndFadeOutActor, ++ InitError, isSelectable, getIconName */ + + const { Clutter, Gio, GLib } = imports.gi; + const Signals = imports.signals; +@@ -125,7 +126,7 @@ function cloneAndFadeOutActor(actor) { + * a service via beginVerification fails. It wraps the underlying error + * and provides context about which service failed. + */ +-export class InitError extends Error { ++var InitError = class extends Error { + constructor(error, message, serviceName) { + super(message, {cause: error}); + this.serviceName = serviceName; +@@ -136,7 +137,7 @@ export class InitError extends Error { + * @param {object} mechanism + * @returns {boolean} + */ +-export function isSelectable(mechanism) { ++function isSelectable(mechanism) { + switch (mechanism.role) { + case Constants.PASSWORD_ROLE_NAME: + case Constants.SMARTCARD_ROLE_NAME: +@@ -154,7 +155,7 @@ export function isSelectable(mechanism) { + * @param {object} mechanism + * @returns {string} + */ +-export function getNonSelectableIconName(mechanism) { ++function getNonSelectableIconName(mechanism) { + // This is only used for non selectable mechanisms. + // Currently only fingerprint is non selectable + if (isSelectable(mechanism)) +diff --git a/js/gdm/webLogin.js b/js/gdm/webLogin.js +index ef4f28b592..9c858f7269 100644 +--- a/js/gdm/webLogin.js ++++ b/js/gdm/webLogin.js +@@ -1,7 +1,7 @@ + // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + // + // A widget showing a URL for web login +-/* exported WebLoginPrompt */ ++/* exported QrCode, WebLoginPrompt, WebLoginDialog, WebLoginIntro */ + + const { Clutter, Gio, GnomeQR, GObject, Pango, St } = imports.gi; + +@@ -15,9 +15,9 @@ const QR_CODE_SIZE = 150; + const WEB_LOGIN_SPINNER_SIZE = 35; + const URL_LABEL_LONG_THRESHOLD = 45; + +-export const WebLoginPrompt = GObject.registerClass( ++var WebLoginPrompt = GObject.registerClass( + class WebLoginPrompt extends St.BoxLayout { +- constructor(params) { ++ _init(params) { + const {qrSize: qrCodeSize, message, url, code} = Params.parse(params, { + qrSize: QR_CODE_SIZE, + message: null, +@@ -25,7 +25,7 @@ class WebLoginPrompt extends St.BoxLayout { + code: null, + }); + +- super({ ++ super._init({ + styleClass: 'web-login-prompt', + vertical: true, + y_align: Clutter.ActorAlign.CENTER, +@@ -124,13 +124,13 @@ class WebLoginPrompt extends St.BoxLayout { + } + }); + +-export const WebLoginDialog = GObject.registerClass({ ++var WebLoginDialog = GObject.registerClass({ + Signals: { + 'cancel': {}, + 'loading': {}, + }, + }, class WebLoginDialog extends St.Widget { +- constructor(params) { ++ _init(params) { + const {message, url, code, buttons} = Params.parse(params, { + message: null, + url: null, +@@ -138,7 +138,7 @@ export const WebLoginDialog = GObject.registerClass({ + buttons: [], + }); + +- super({ ++ super._init({ + layout_manager: new Clutter.BinLayout(), + x_expand: true, + y_expand: true, +@@ -279,9 +279,9 @@ export const WebLoginDialog = GObject.registerClass({ + } + }); + +-export var WebLoginIntro = GObject.registerClass( ++var WebLoginIntro = GObject.registerClass( + class WebLoginIntro extends St.Button { +- constructor(params) { ++ _init(params) { + const {message} = Params.parse(params, { + message: null, + }); +@@ -291,7 +291,7 @@ class WebLoginIntro extends St.Button { + style_class: 'web-login-button-label', + }); + +- super({ ++ super._init({ + style_class: 'web-login-intro-button', + accessible_name: message, + button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE, +diff --git a/js/misc/fingerprintManager.js b/js/misc/fingerprintManager.js +index cdeb3579d1..151e787e0b 100644 +--- a/js/misc/fingerprintManager.js ++++ b/js/misc/fingerprintManager.js +@@ -1,4 +1,5 @@ + const { Gio, GLib, GObject } = imports.gi; ++/* exported FingerprintReaderType, FingerprintManager */ + + const FileUtils = imports.misc.fileUtils; + +@@ -7,7 +8,7 @@ const FprintManagerInfo = Gio.DBusInterfaceInfo.new_for_xml( + const FprintDeviceInfo = Gio.DBusInterfaceInfo.new_for_xml( + FileUtils.loadInterfaceXML('net.reactivated.Fprint.Device')); + +-export const FingerprintReaderType = { ++var FingerprintReaderType = { + NONE: 0, + PRESS: 1, + SWIPE: 2, +@@ -18,32 +19,27 @@ let _fingerprintManager = null; + /** + * @returns {FingerprintManager} + */ +-export function getFingerprintManager() { ++function getFingerprintManager() { + if (_fingerprintManager == null) + _fingerprintManager = new FingerprintManager(); + + return _fingerprintManager; + } + +-class FingerprintManager extends GObject.Object { +- static [GObject.properties] = { ++var FingerprintManager = GObject.registerClass({ ++ Properties: { + 'reader-type': GObject.ParamSpec.uint( +- 'reader-type', null, null, ++ 'reader-type', 'reader-type', 'reader-type', + GObject.ParamFlags.READWRITE, + FingerprintReaderType.NONE, FingerprintReaderType.SWIPE, + FingerprintReaderType.NONE), +- }; +- +- static [GObject.signals] = { ++ }, ++ Signals: { + 'reader-type-changed': {}, +- }; +- +- static { +- GObject.registerClass(this); +- } +- +- constructor() { +- super(); ++ }, ++}, class FingerprintManager extends GObject.Object { ++ _init() { ++ super._init(); + + this._fingerprintManagerProxy = new Gio.DBusProxy({ + g_connection: Gio.DBus.system, +@@ -133,4 +129,4 @@ class FingerprintManager extends GObject.Object { + + logError(e, 'Failed to interact with fprintd service'); + } +-} ++}); +diff --git a/js/misc/passkeyDeviceManager.js b/js/misc/passkeyDeviceManager.js +index 2aa81d2ce6..747ee93fc8 100644 +--- a/js/misc/passkeyDeviceManager.js ++++ b/js/misc/passkeyDeviceManager.js +@@ -1,30 +1,27 @@ + const { GObject, GUdev } = imports.gi; ++/* exported getPasskeyDeviceManager */ ++ + + let _passkeyDeviceManager = null; + + /** + * @returns {PasskeyDeviceManager} + */ +-export function getPasskeyDeviceManager() { ++function getPasskeyDeviceManager() { + if (_passkeyDeviceManager == null) + _passkeyDeviceManager = new PasskeyDeviceManager(); + + return _passkeyDeviceManager; + } + +-class PasskeyDeviceManager extends GObject.Object { +- static [GObject.signals] = { ++var PasskeyDeviceManager = GObject.registerClass({ ++ Signals: { + 'passkey-inserted': {param_types: [GObject.TYPE_JSOBJECT]}, + 'passkey-removed': {param_types: [GObject.TYPE_JSOBJECT]}, +- }; +- +- static { +- GObject.registerClass(this); +- } +- +- constructor() { +- super(); +- ++ }, ++}, class PasskeyDeviceManager extends GObject.Object { ++ _init() { ++ super._init(); + this._insertedPasskeys = new Map(); + this._udevClient = new GUdev.Client({subsystems: ['hidraw']}); + +@@ -65,4 +62,4 @@ class PasskeyDeviceManager extends GObject.Object { + this._insertedPasskeys.delete(sysfsPath); + this.emit('passkey-removed', device); + } +-} ++}); +diff --git a/js/ui/qrCode.js b/js/ui/qrCode.js +index d1fac46847..5920e45e5a 100644 +--- a/js/ui/qrCode.js ++++ b/js/ui/qrCode.js +@@ -7,43 +7,39 @@ 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.properties] = { ++var QrCode = GObject.registerClass({ ++ Properties: { + 'url': GObject.ParamSpec.string( +- 'url', null, null, ++ 'url', 'url', 'url', + GObject.ParamFlags.READWRITE, + null), +- }; +- +- static { +- GObject.registerClass(this); +- } +- +- constructor(params) { ++ }, ++}, class QrCode extends St.Bin { ++ _init(params) { + const qrSize = Math.max(params.width ?? 0, params.height ?? 0) || + QR_CODE_DEFAULT_SIZE; + +- super({ +- styleClass: 'qr-code', ++ super._init({ ++ style_class: '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, ++ x_expand: true, ++ y_expand: true, ++ x_align: Clutter.ActorAlign.FILL, ++ y_align: Clutter.ActorAlign.FILL, ++ minification_filter: Clutter.ScalingFilter.NEAREST, ++ magnification_filter: Clutter.ScalingFilter.NEAREST, + })); + + this._fgColor = null; + + this.connect('notify::url', () => + this._update().catch(logErrorUnlessCancelled)); +- }; ++ } + + vfunc_allocate(box) { + const width = box.get_width(); +@@ -69,7 +65,7 @@ export class QrCode extends St.Bin { + } + + async _update() { +- const fgColor = QrCode.#getGnomeQRColor(this._fgColor); ++ const fgColor = QrCode._getGnomeQRColor(this._fgColor); + + this._cancellable?.cancel(); + const cancellable = new Gio.Cancellable(); +@@ -100,7 +96,7 @@ export class QrCode extends St.Bin { + this.child.set_content(content); + } + +- static #getGnomeQRColor(color) { ++ static _getGnomeQRColor(color) { + if (!color) + return null; + +@@ -111,4 +107,4 @@ export class QrCode extends St.Bin { + alpha: color.alpha, + }); + } +-}; ++}); +-- +2.51.0 + + +From 8d635a7b5f8da824025ae9e206cd2cdf92bc83ff Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Fri, 26 Dec 2025 15:01:19 +0100 +Subject: [PATCH 04/30] gdm: Fix using logErrorUnless cancelled + +This function doesn't exist in this version. +--- + js/gdm/authServices.js | 3 ++- + js/gdm/authServicesLegacy.js | 12 +++++++++--- + js/gdm/authServicesSSSDSwitchable.js | 12 +++++++++--- + js/gdm/util.js | 5 ++++- + js/ui/qrCode.js | 13 ++++++++++--- + 5 files changed, 34 insertions(+), 11 deletions(-) + +diff --git a/js/gdm/authServices.js b/js/gdm/authServices.js +index ce4f0729b3..fea7ba0634 100644 +--- a/js/gdm/authServices.js ++++ b/js/gdm/authServices.js +@@ -373,7 +373,8 @@ var 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/authServicesLegacy.js b/js/gdm/authServicesLegacy.js +index 8922546474..0fd0e10d81 100644 +--- a/js/gdm/authServicesLegacy.js ++++ b/js/gdm/authServicesLegacy.js +@@ -1,6 +1,6 @@ + /* exported AuthServicesLegacy */ + +-const { GLib, GObject } = imports.gi; ++const { Gio, GLib, GObject } = imports.gi; + + const {AuthServices} = imports.gdm.authServices; + const Constants = imports.gdm.constants; +@@ -61,7 +61,10 @@ var AuthServicesLegacy = GObject.registerClass({ + return; + + this._userVerifierChoiceList.call_select_choice( +- serviceName, key, this._cancellable).catch(logErrorUnlessCancelled); ++ serviceName, key, this._cancellable).catch(e => { ++ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) ++ logError(e); ++ }); + } + + _handleAnswerQuery(serviceName, answer) { +@@ -72,7 +75,10 @@ var AuthServicesLegacy = GObject.registerClass({ + this._smartcardInProgress = true; + + this._userVerifier.call_answer_query( +- serviceName, answer, this._cancellable).catch(logErrorUnlessCancelled); ++ serviceName, answer, this._cancellable).catch(e => { ++ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) ++ logError(e); ++ }); + } + + _handleBeginVerification() { +diff --git a/js/gdm/authServicesSSSDSwitchable.js b/js/gdm/authServicesSSSDSwitchable.js +index 4b4761d105..601f4cd295 100644 +--- a/js/gdm/authServicesSSSDSwitchable.js ++++ b/js/gdm/authServicesSSSDSwitchable.js +@@ -1,6 +1,6 @@ + /* exported AuthServicesSwitchable */ + +-const { GLib, GObject } = imports.gi; ++const { Gio, GLib, GObject } = imports.gi; + + const {AuthServices} = imports.gdm.authServices; + const Constants = imports.gdm.constants; +@@ -54,7 +54,10 @@ var AuthServicesSSSDSwitchable = GObject.registerClass({ + this._resettingPassword) { + this._userVerifier.call_answer_query(serviceName, + answer, +- this._cancellable).catch(logErrorUnlessCancelled); ++ this._cancellable).catch(e => { ++ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) ++ logError(e); ++ }); + return; + } + +@@ -316,7 +319,10 @@ var AuthServicesSSSDSwitchable = GObject.registerClass({ + const {serviceName} = this._selectedMechanism; + + this._userVerifierCustomJSON.call_reply( +- serviceName, JSON.stringify(response), this._cancellable).catch(logErrorUnlessCancelled); ++ serviceName, JSON.stringify(response), this._cancellable).catch(e => { ++ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) ++ logError(e); ++ }); + } + + _eventExpected() { +diff --git a/js/gdm/util.js b/js/gdm/util.js +index f1709edea3..17892c75e8 100644 +--- a/js/gdm/util.js ++++ b/js/gdm/util.js +@@ -282,7 +282,10 @@ var ShellUserVerifier = class { + async answerQuery(serviceName, answer) { + // Wait for pending messages to be displayed before answering to + // ensure no messages get lost +- await this._handlePendingMessages().catch(logErrorUnlessCancelled); ++ await this._handlePendingMessages().catch(e => { ++ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) ++ logError(e); ++ }); + + this._authServicesSSSDSwitchable?.answerQuery(serviceName, answer); + this._authServicesLegacy?.answerQuery(serviceName, answer); +diff --git a/js/ui/qrCode.js b/js/ui/qrCode.js +index 5920e45e5a..b53508c2dd 100644 +--- a/js/ui/qrCode.js ++++ b/js/ui/qrCode.js +@@ -37,8 +37,12 @@ var QrCode = GObject.registerClass({ + + this._fgColor = null; + +- this.connect('notify::url', () => +- this._update().catch(logErrorUnlessCancelled)); ++ this.connect('notify::url', () => { ++ this._update().catch(e => { ++ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) ++ logError(e); ++ }); ++ }); + } + + vfunc_allocate(box) { +@@ -60,7 +64,10 @@ var QrCode = GObject.registerClass({ + + if (!this._fgColor?.equal(fgColor)) { + this._fgColor = fgColor; +- this._update().catch(logErrorUnlessCancelled); ++ this._update().catch(e => { ++ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) ++ logError(e); ++ }); + } + } + +-- +2.51.0 + + +From 147ad05d7d926478da5d35ea14bcec81bf0795ee Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Fri, 12 Dec 2025 12:58:59 +0100 +Subject: [PATCH 05/30] gdm: Change connectObject to connect + +connectObject is a helper that isn't available in this older version. +--- + js/gdm/authPrompt.js | 24 +++--- + js/gdm/authServices.js | 138 +++++++++++++++++++++++++---------- + js/gdm/authServicesLegacy.js | 26 ++++++- + js/gdm/util.js | 61 +++++++++++----- + js/gdm/webLogin.js | 11 ++- + 5 files changed, 186 insertions(+), 74 deletions(-) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index 8c5c8a1fab..6996dac18b 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -81,16 +81,16 @@ var AuthPrompt = GObject.registerClass({ + + this._userVerifier = new GdmUtil.ShellUserVerifier(this._gdmClient, { reauthenticationOnly }); + +- this._userVerifier.connectObject( +- 'ask-question', this._onAskQuestion.bind(this), +- 'show-message', this._onShowMessage.bind(this), +- 'show-choice-list', this._onShowChoiceList.bind(this), +- 'mechanisms-changed', (_, ...args) => this.emit('mechanisms-changed', ...args), +- 'web-login', this._onWebLogin.bind(this), +- 'verification-failed', this._onVerificationFailed.bind(this), +- 'verification-complete', this._onVerificationComplete.bind(this), +- 'reset', this._onReset.bind(this), +- this); ++ this._userVerifierSignalIds = [ ++ this._userVerifier.connect('ask-question', this._onAskQuestion.bind(this)), ++ this._userVerifier.connect('show-message', this._onShowMessage.bind(this)), ++ this._userVerifier.connect('show-choice-list', this._onShowChoiceList.bind(this)), ++ this._userVerifier.connect('mechanisms-changed', (_, ...args) => this.emit('mechanisms-changed', ...args)), ++ this._userVerifier.connect('web-login', this._onWebLogin.bind(this)), ++ this._userVerifier.connect('verification-failed', this._onVerificationFailed.bind(this)), ++ this._userVerifier.connect('verification-complete', this._onVerificationComplete.bind(this)), ++ this._userVerifier.connect('reset', this._onReset.bind(this)), ++ ]; + + this.connect('destroy', this._onDestroy.bind(this)); + +@@ -134,7 +134,9 @@ var AuthPrompt = GObject.registerClass({ + this._preemptiveAnswerWatchId = 0; + } + +- this._userVerifier.disconnectObject(this); ++ this._userVerifierSignalIds.forEach(id => ++ this._userVerifier.disconnect(id)); ++ this._userVerifierSignalIds = []; + this._userVerifier.destroy(); + this._userVerifier = null; + this._entry = null; +diff --git a/js/gdm/authServices.js b/js/gdm/authServices.js +index fea7ba0634..88462cf17e 100644 +--- a/js/gdm/authServices.js ++++ b/js/gdm/authServices.js +@@ -165,6 +165,8 @@ var AuthServices = GObject.registerClass({ + destroy() { + this.reset(); + this.clear(); ++ this._disconnectManagers(); ++ this._handleDestroy(); + this.emit('destroy'); + } + +@@ -202,10 +204,35 @@ var AuthServices = GObject.registerClass({ + } + + _disconnectUserVerifierSignals() { +- this._userVerifier?.get_connection().disconnectObject(this); +- this._userVerifier?.disconnectObject(this); +- this._userVerifierChoiceList?.disconnectObject(this); +- this._userVerifierCustomJSON?.disconnectObject(this); ++ this._userVerifierConnectionSignalIds?.forEach(id => ++ this._userVerifier?.get_connection().disconnect(id)); ++ this._userVerifierConnectionSignalIds = []; ++ ++ this._userVerifierSignalIds?.forEach(id => ++ this._userVerifier?.disconnect(id)); ++ this._userVerifierSignalIds = []; ++ ++ this._userVerifierChoiceListSignalIds?.forEach(id => ++ this._userVerifierChoiceList?.disconnect(id)); ++ this._userVerifierChoiceListSignalIds = []; ++ ++ this._userVerifierCustomJSONSignalIds?.forEach(id => ++ this._userVerifierCustomJSON?.disconnect(id)); ++ this._userVerifierCustomJSONSignalIds = []; ++ } ++ ++ _disconnectManagers() { ++ this._smartcardManagerSignalIds?.forEach(id => ++ this._smartcardManager?.disconnect(id)); ++ this._smartcardManagerSignalIds = []; ++ ++ this._passkeyDeviceManagerSignalIds?.forEach(id => ++ this._passkeyDeviceManager?.disconnect(id)); ++ this._passkeyDeviceManagerSignalIds = []; ++ ++ this._fingerprintManagerSignalIds?.forEach(id => ++ this._fingerprintManager?.disconnect(id)); ++ this._fingerprintManagerSignalIds = []; + } + + _updateEnabledMechanisms() { +@@ -218,18 +245,24 @@ var AuthServices = GObject.registerClass({ + + _connectSmartcardManager() { + this._smartcardManager = SmartcardManager.getSmartcardManager(); +- this._smartcardManager.connectObject( +- 'smartcard-inserted', () => this._handleSmartcardChanged(), +- 'smartcard-removed', () => this._handleSmartcardChanged(), +- this); ++ this._smartcardManagerSignalIds = []; ++ let id = this._smartcardManager.connect('smartcard-inserted', ++ () => this._handleSmartcardChanged()); ++ this._smartcardManagerSignalIds.push(id); ++ id = this._smartcardManager.connect('smartcard-removed', ++ () => this._handleSmartcardChanged()); ++ this._smartcardManagerSignalIds.push(id); + } + + _connectPasskeyDeviceManager() { + this._passkeyDeviceManager = PasskeyDeviceManager.getPasskeyDeviceManager(); +- this._passkeyDeviceManager.connectObject( +- 'passkey-inserted', () => this._handlePasskeyChanged(), +- 'passkey-removed', () => this._handlePasskeyChanged(), +- this); ++ this._passkeyDeviceManagerSignalIds = []; ++ let id = this._passkeyDeviceManager.connect('passkey-inserted', ++ () => this._handlePasskeyChanged()); ++ this._passkeyDeviceManagerSignalIds.push(id); ++ id = this._passkeyDeviceManager.connect('passkey-removed', ++ () => this._handlePasskeyChanged()); ++ this._passkeyDeviceManagerSignalIds.push(id); + } + + _connectFingerprintManager() { +@@ -237,10 +270,11 @@ var AuthServices = GObject.registerClass({ + if (!this._reauthOnly) + return; + +- this._fingerprintManager = FingerprintManager.getFingerprintManager(); +- this._fingerprintManager.connectObject( +- 'reader-type-changed', () => this._handleFingerprintChanged(), +- this); ++ this._fingerprintManager = new FingerprintManager.FingerprintManager(); ++ this._fingerprintManagerSignalIds = []; ++ let id = this._fingerprintManager.connect('reader-type-changed', ++ () => this._handleFingerprintChanged()); ++ this._fingerprintManagerSignalIds.push(id); + } + + _waitPendingMessages() { +@@ -276,29 +310,53 @@ var AuthServices = GObject.registerClass({ + } + + _connectUserVerifierSignals() { +- this._userVerifier.get_connection().connectObject( +- 'closed', () => this._clearUserVerifier(), +- this); +- +- this._userVerifier.connectObject( +- 'info', (_, ...args) => this._onInfo(...args), +- 'problem', (_, ...args) => this._onProblem(...args), +- 'info-query', (_, ...args) => this._onInfoQuery(...args), +- 'secret-info-query', (_, ...args) => this._onSecretInfoQuery(...args), +- 'conversation-started', (_, ...args) => this._onConversationStarted(...args), +- 'conversation-stopped', (_, ...args) => this._onConversationStopped(...args), +- 'service-unavailable', (_, ...args) => this._onServiceUnavailable(...args), +- 'reset', () => this.emit('reset', {}), +- 'verification-complete', (_, ...args) => this._onVerificationComplete(...args), +- this); +- +- this._userVerifierChoiceList?.connectObject( +- 'choice-query', (_, ...args) => this._onChoiceListQuery(...args), +- this); +- +- this._userVerifierCustomJSON?.connectObject( +- 'request', (_, ...args) => this._onCustomJSONRequest(...args), +- this); ++ this._userVerifierConnectionSignalIds = []; ++ let id = this._userVerifier.get_connection().connect('closed', ++ () => this._clearUserVerifier()); ++ this._userVerifierConnectionSignalIds.push(id); ++ ++ this._userVerifierSignalIds = []; ++ id = this._userVerifier.connect('info', ++ (_, ...args) => this._onInfo(...args)); ++ this._userVerifierSignalIds.push(id); ++ id = this._userVerifier.connect('problem', ++ (_, ...args) => this._onProblem(...args)); ++ this._userVerifierSignalIds.push(id); ++ id = this._userVerifier.connect('info-query', ++ (_, ...args) => this._onInfoQuery(...args)); ++ this._userVerifierSignalIds.push(id); ++ id = this._userVerifier.connect('secret-info-query', ++ (_, ...args) => this._onSecretInfoQuery(...args)); ++ this._userVerifierSignalIds.push(id); ++ id = this._userVerifier.connect('conversation-started', ++ (_, ...args) => this._onConversationStarted(...args)); ++ this._userVerifierSignalIds.push(id); ++ id = this._userVerifier.connect('conversation-stopped', ++ (_, ...args) => this._onConversationStopped(...args)); ++ this._userVerifierSignalIds.push(id); ++ id = this._userVerifier.connect('service-unavailable', ++ (_, ...args) => this._onServiceUnavailable(...args)); ++ this._userVerifierSignalIds.push(id); ++ id = this._userVerifier.connect('reset', ++ () => this.emit('reset', {})); ++ this._userVerifierSignalIds.push(id); ++ id = this._userVerifier.connect('verification-complete', ++ (_, ...args) => this._onVerificationComplete(...args)); ++ this._userVerifierSignalIds.push(id); ++ ++ this._userVerifierChoiceListSignalIds = []; ++ if (this._userVerifierChoiceList) { ++ id = this._userVerifierChoiceList.connect('choice-query', ++ (_, ...args) => this._onChoiceListQuery(...args)); ++ this._userVerifierChoiceListSignalIds.push(id); ++ } ++ ++ this._userVerifierCustomJSONSignalIds = []; ++ if (this._userVerifierCustomJSON) { ++ id = this._userVerifierCustomJSON.connect('request', ++ (_, ...args) => this._onCustomJSONRequest(...args)); ++ this._userVerifierCustomJSONSignalIds.push(id); ++ } + } + + _onInfo(serviceName, info) { +@@ -456,6 +514,8 @@ var AuthServices = GObject.registerClass({ + + _handleClear() {} + ++ _handleDestroy() {} ++ + _handleUpdateEnabledRoles() {} + + _handleUpdateEnabledMechanisms() { +diff --git a/js/gdm/authServicesLegacy.js b/js/gdm/authServicesLegacy.js +index 0fd0e10d81..5955aeadff 100644 +--- a/js/gdm/authServicesLegacy.js ++++ b/js/gdm/authServicesLegacy.js +@@ -50,6 +50,7 @@ var AuthServicesLegacy = GObject.registerClass({ + this._updateEnabledMechanisms(); + + this._credentialManagers = {}; ++ this._credentialManagerSignalIds = {}; + this.addCredentialManager(OVirt.SERVICE_NAME, OVirt.getOVirtCredentialsManager()); + this.addCredentialManager(Vmware.SERVICE_NAME, Vmware.getVmwareCredentialsManager()); + +@@ -104,6 +105,20 @@ var AuthServicesLegacy = GObject.registerClass({ + this._selectedMechanism = null; + } + ++ _handleDestroy() { ++ this._disconnectCredentialManagers(); ++ } ++ ++ _disconnectCredentialManagers() { ++ Object.keys(this._credentialManagerSignalIds).forEach(serviceName => { ++ const signalId = this._credentialManagerSignalIds[serviceName]; ++ const credentialManager = this._credentialManagers[serviceName]; ++ if (signalId) ++ credentialManager?.disconnect(signalId); ++ }); ++ this._credentialManagerSignalIds = {}; ++ } ++ + _handleClear() { + this._smartcardInProgress = false; + this._clearFingerprintReadyTimeout(); +@@ -359,9 +374,9 @@ var AuthServicesLegacy = GObject.registerClass({ + if (credentialManager.token) + this._onCredentialManagerAuthenticated(credentialManager); + +- credentialManager.connectObject( +- 'user-authenticated', () => this._onCredentialManagerAuthenticated(credentialManager), +- this); ++ const id = credentialManager.connect('user-authenticated', ++ () => this._onCredentialManagerAuthenticated(credentialManager)); ++ this._credentialManagerSignalIds[serviceName] = id; + } + + removeCredentialManager(serviceName) { +@@ -369,7 +384,10 @@ var AuthServicesLegacy = GObject.registerClass({ + if (!credentialManager) + return; + +- credentialManager.disconnectObject(this); ++ const signalId = this._credentialManagerSignalIds[serviceName]; ++ if (signalId) ++ credentialManager.disconnect(signalId); ++ delete this._credentialManagerSignalIds[serviceName]; + delete this._credentialManagers[serviceName]; + + if (this._selectedMechanism?.serviceName === serviceName) +diff --git a/js/gdm/util.js b/js/gdm/util.js +index 17892c75e8..85de597d33 100644 +--- a/js/gdm/util.js ++++ b/js/gdm/util.js +@@ -270,6 +270,8 @@ var ShellUserVerifier = class { + + this.cancel(); + ++ this._clearAuthServices(); ++ + this._settings.run_dispose(); + this._settings = null; + } +@@ -498,31 +500,54 @@ var ShellUserVerifier = class { + } + + _clearAuthServices() { ++ this._authServicesSSSDSwitchableSignalIds?.forEach(id => ++ this._authServicesSSSDSwitchable?.disconnect(id)); ++ this._authServicesSSSDSwitchableSignalIds = []; + this._authServicesSSSDSwitchable?.destroy(); + this._authServicesSSSDSwitchable = null; ++ ++ this._authServicesLegacySignalIds?.forEach(id => ++ this._authServicesLegacy?.disconnect(id)); ++ this._authServicesLegacySignalIds = []; + this._authServicesLegacy?.destroy(); + this._authServicesLegacy = null; + } + + _connectAuthServices() { +- [ +- this._authServicesSSSDSwitchable, +- this._authServicesLegacy, +- ].forEach(authServices => { +- authServices?.connectObject( +- 'ask-question', (_, ...args) => this.emit('ask-question', ...args), +- 'queue-message', (_, ...args) => this._queueMessage(...args), +- 'queue-priority-message', (_, ...args) => this._queuePriorityMessage(...args), +- 'wait-pending-messages', (_, ...args) => this._waitPendingMessages(...args), +- 'filter-messages', (_, ...args) => this._filterServiceMessages(...args), +- 'verification-failed', (_, ...args) => this._verificationFailed(...args), +- 'verification-complete', (_, ...args) => this.emit('verification-complete', ...args), +- 'reset', (_, ...args) => this.emit('reset', ...args), +- 'show-choice-list', (_, ...args) => this.emit('show-choice-list', ...args), +- 'mechanisms-changed', (_, ...args) => this._onMechanismsChanged(...args), +- 'web-login', (_, ...args) => this.emit('web-login', ...args), +- this); +- }); ++ const connectSignals = authServices => { ++ if (!authServices) ++ return []; ++ ++ return [ ++ authServices.connect('ask-question', ++ (_, ...args) => this.emit('ask-question', ...args)), ++ authServices.connect('queue-message', ++ (_, ...args) => this._queueMessage(...args)), ++ authServices.connect('queue-priority-message', ++ (_, ...args) => this._queuePriorityMessage(...args)), ++ authServices.connect('wait-pending-messages', ++ (_, ...args) => this._waitPendingMessages(...args)), ++ authServices.connect('filter-messages', ++ (_, ...args) => this._filterServiceMessages(...args)), ++ authServices.connect('verification-failed', ++ (_, ...args) => this._verificationFailed(...args)), ++ authServices.connect('verification-complete', ++ (_, ...args) => this.emit('verification-complete', ...args)), ++ authServices.connect('reset', ++ (_, ...args) => this.emit('reset', ...args)), ++ authServices.connect('show-choice-list', ++ (_, ...args) => this.emit('show-choice-list', ...args)), ++ authServices.connect('mechanisms-changed', ++ (_, ...args) => this._onMechanismsChanged(...args)), ++ authServices.connect('web-login', ++ (_, ...args) => this.emit('web-login', ...args)), ++ ]; ++ }; ++ ++ this._authServicesSSSDSwitchableSignalIds = ++ connectSignals(this._authServicesSSSDSwitchable); ++ this._authServicesLegacySignalIds = ++ connectSignals(this._authServicesLegacy); + } + + _verificationFailed(serviceName, canRetry) { +diff --git a/js/gdm/webLogin.js b/js/gdm/webLogin.js +index 9c858f7269..26da94b624 100644 +--- a/js/gdm/webLogin.js ++++ b/js/gdm/webLogin.js +@@ -182,7 +182,14 @@ var WebLoginDialog = GObject.registerClass({ + } + + _updateButtons(buttons) { ++ this._buttonBox.get_children().forEach(b => { ++ if (b._clickedSignalId) { ++ b.disconnect(b._clickedSignalId); ++ b._clickedSignalId = 0; ++ } ++ }); + this._buttonBox.destroy_all_children(); ++ + this._cancelButton = this._addButton({ + label: _('Cancel'), + action: () => this.emit('cancel'), +@@ -202,11 +209,11 @@ var WebLoginDialog = GObject.registerClass({ + }), + }); + +- button.connectObject('clicked', () => { ++ button._clickedSignalId = button.connect('clicked', () => { + if (b.needsLoading) + this._startLoading(); + b.action(); +- }, this); ++ }); + + button.default = b.default; + +-- +2.51.0 + + +From bcf392cbdac51d1bb1fb2e161c674bc4bd3286a6 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Fri, 26 Dec 2025 15:12:16 +0100 +Subject: [PATCH 06/30] gdm: Use promisify adding two params + +This is how it works in this old version +--- + js/gdm/authServices.js | 12 +++++------- + js/gdm/util.js | 7 ++++++- + js/gdm/webLogin.js | 2 +- + js/ui/qrCode.js | 2 +- + 4 files changed, 13 insertions(+), 10 deletions(-) + +diff --git a/js/gdm/authServices.js b/js/gdm/authServices.js +index 88462cf17e..3cb1985817 100644 +--- a/js/gdm/authServices.js ++++ b/js/gdm/authServices.js +@@ -9,13 +9,11 @@ const PasskeyDeviceManager = imports.misc.passkeyDeviceManager; + const SmartcardManager = imports.misc.smartcardManager; + const Util = imports.gdm.util; + +-Gio._promisify(Gdm.Client.prototype, 'open_reauthentication_channel'); +-Gio._promisify(Gdm.Client.prototype, 'get_user_verifier'); +-Gio._promisify(Gdm.UserVerifierProxy.prototype, 'call_begin_verification_for_user'); +-Gio._promisify(Gdm.UserVerifierProxy.prototype, 'call_begin_verification'); +-Gio._promisify(Gdm.UserVerifierProxy.prototype, 'call_answer_query'); +-Gio._promisify(Gdm.UserVerifierChoiceListProxy.prototype, 'call_select_choice'); +-Gio._promisify(Gdm.UserVerifierCustomJSONProxy.prototype, 'call_reply'); ++Gio._promisify(Gdm.UserVerifierProxy.prototype, 'call_begin_verification_for_user', 'call_begin_verification_for_user_finish'); ++Gio._promisify(Gdm.UserVerifierProxy.prototype, 'call_begin_verification', 'call_begin_verification_finish'); ++Gio._promisify(Gdm.UserVerifierProxy.prototype, 'call_answer_query', 'call_answer_query_finish'); ++Gio._promisify(Gdm.UserVerifierChoiceListProxy.prototype, 'call_select_choice', 'call_select_choice_finish'); ++Gio._promisify(Gdm.UserVerifierCustomJSONProxy.prototype, 'call_reply', 'call_reply_finish'); + + var AuthServices = GObject.registerClass({ + Signals: { +diff --git a/js/gdm/util.js b/js/gdm/util.js +index 85de597d33..4aeec5b02a 100644 +--- a/js/gdm/util.js ++++ b/js/gdm/util.js +@@ -3,7 +3,7 @@ + DISABLE_USER_LIST_KEY, fadeInActor, fadeOutActor, cloneAndFadeOutActor, + InitError, isSelectable, getIconName */ + +-const { Clutter, Gio, GLib } = imports.gi; ++const { Clutter, Gdm, Gio, GLib } = imports.gi; + const Signals = imports.signals; + + const Batch = imports.gdm.batch; +@@ -13,6 +13,11 @@ const Params = imports.misc.params; + const { AuthServicesLegacy } = imports.gdm.authServicesLegacy; + const { AuthServicesSSSDSwitchable } = imports.gdm.authServicesSSSDSwitchable; + ++Gio._promisify(Gdm.Client.prototype, 'open_reauthentication_channel', 'open_reauthentication_channel_finish'); ++Gio._promisify(Gdm.Client.prototype, 'get_user_verifier', 'get_user_verifier_finish'); ++Gio._promisify(Gdm.UserVerifierProxy.prototype, 'call_begin_verification_for_user', 'call_begin_verification_for_user_finish'); ++Gio._promisify(Gdm.UserVerifierProxy.prototype, 'call_begin_verification', 'call_begin_verification_finish'); ++ + var FADE_ANIMATION_TIME = 160; + var CLONE_FADE_ANIMATION_TIME = 250; + +diff --git a/js/gdm/webLogin.js b/js/gdm/webLogin.js +index 26da94b624..f4972144fa 100644 +--- a/js/gdm/webLogin.js ++++ b/js/gdm/webLogin.js +@@ -9,7 +9,7 @@ const Params = imports.misc.params; + const { Spinner } = imports.ui.animation; + const { QrCode } = imports.ui.qrCode; + +-Gio._promisify(GnomeQR, 'generate_qr_code_async'); ++Gio._promisify(GnomeQR, 'generate_qr_code_async', 'generate_qr_code_finish'); + + const QR_CODE_SIZE = 150; + const WEB_LOGIN_SPINNER_SIZE = 35; +diff --git a/js/ui/qrCode.js b/js/ui/qrCode.js +index b53508c2dd..547ad7d153 100644 +--- a/js/ui/qrCode.js ++++ b/js/ui/qrCode.js +@@ -2,7 +2,7 @@ + + const { Clutter, Cogl, Gio, GnomeQR, GObject, St } = imports.gi; + +-Gio._promisify(GnomeQR, 'generate_qr_code_async'); ++Gio._promisify(GnomeQR, 'generate_qr_code_async', 'generate_qr_code_finish'); + + const QR_CODE_DEFAULT_SIZE = 150; + const QR_CODE_TRANSPARENT_COLOR = new GnomeQR.Color({alpha: 0}); +-- +2.51.0 + + +From 74a94cb0c34413d45ef2e1d599822c25c434e1b8 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Fri, 27 Mar 2026 13:52:50 +0100 +Subject: [PATCH 07/30] gdm: Use timeout_add_seconds instead of once + +_once type doesn't exist in this old glib version +--- + js/gdm/authServices.js | 7 +++++-- + js/gdm/authServicesSSSDSwitchable.js | 3 ++- + 2 files changed, 7 insertions(+), 3 deletions(-) + +diff --git a/js/gdm/authServices.js b/js/gdm/authServices.js +index 3cb1985817..003b4836a6 100644 +--- a/js/gdm/authServices.js ++++ b/js/gdm/authServices.js +@@ -277,8 +277,11 @@ var AuthServices = GObject.registerClass({ + + _waitPendingMessages() { + const cancellable = this._cancellable; +- const timeoutId = GLib.timeout_add_seconds_once(GLib.PRIORITY_DEFAULT, 10, +- () => cancellable.cancel()); ++ const timeoutId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 10, ++ () => { ++ cancellable.cancel(); ++ return GLib.SOURCE_REMOVE; ++ }); + + const {promise, resolve, reject} = Promise.withResolvers(); + const task = Gio.Task.new(this, cancellable, () => { +diff --git a/js/gdm/authServicesSSSDSwitchable.js b/js/gdm/authServicesSSSDSwitchable.js +index 601f4cd295..48c1c020fb 100644 +--- a/js/gdm/authServicesSSSDSwitchable.js ++++ b/js/gdm/authServicesSSSDSwitchable.js +@@ -195,7 +195,7 @@ var AuthServicesSSSDSwitchable = GObject.registerClass({ + if (!timeout) + return; + +- this._webLoginTimeoutId = GLib.timeout_add_seconds_once(GLib.PRIORITY_DEFAULT, ++ this._webLoginTimeoutId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, + timeout, () => { + if (this._selectedMechanism?.role !== Constants.WEB_LOGIN_ROLE_NAME) + webLoginMechanism.needsRefresh = true; +@@ -203,6 +203,7 @@ var AuthServicesSSSDSwitchable = GObject.registerClass({ + this.emit('reset', {softReset: true}); + + this._webLoginTimeoutId = 0; ++ return GLib.SOURCE_REMOVE; + }); + } + +-- +2.51.0 + + +From aff9353c2f80c15111f9b7f772b5fa45277251fd Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Fri, 27 Mar 2026 13:56:56 +0100 +Subject: [PATCH 08/30] gdm: fix Promise.withResolvers + +Use manual new Promise to extract them. +--- + js/gdm/authServices.js | 6 +++++- + 1 file changed, 5 insertions(+), 1 deletion(-) + +diff --git a/js/gdm/authServices.js b/js/gdm/authServices.js +index 003b4836a6..00d19004f3 100644 +--- a/js/gdm/authServices.js ++++ b/js/gdm/authServices.js +@@ -283,7 +283,11 @@ var AuthServices = GObject.registerClass({ + return GLib.SOURCE_REMOVE; + }); + +- const {promise, resolve, reject} = Promise.withResolvers(); ++ let resolve, reject; ++ const promise = new Promise((_resolve, _reject) => { ++ resolve = _resolve; ++ reject = _reject; ++ }); + const task = Gio.Task.new(this, cancellable, () => { + try { + const res = task.propagate_boolean(); +-- +2.51.0 + + +From fdad9ac3e5bb6699d835d23a50033244fefc6907 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Fri, 12 Dec 2025 19:33:38 +0100 +Subject: [PATCH 09/30] data/theme: Add new icons to theme gresource + +They were in a different icons gresource that doesn't exist in 40 vesion +--- + data/gnome-shell-theme.gresource.xml | 2 ++ + data/icons/meson.build | 1 + + 2 files changed, 3 insertions(+) + +diff --git a/data/gnome-shell-theme.gresource.xml b/data/gnome-shell-theme.gresource.xml +index 7857f4407b..4f697f88df 100644 +--- a/data/gnome-shell-theme.gresource.xml ++++ b/data/gnome-shell-theme.gresource.xml +@@ -4,6 +4,8 @@ + calendar-today.svg + carousel-arrow-next-24-symbolic.svg + carousel-arrow-back-24-symbolic.svg ++ ../icons/scalable/status/fingerprint-auth-symbolic.svg ++ ../icons/scalable/status/vcard-symbolic.svg + checkbox-focused.svg + checkbox-off-focused.svg + checkbox-off.svg +diff --git a/data/icons/meson.build b/data/icons/meson.build +index eff6e4b530..44e2b164a3 100644 +--- a/data/icons/meson.build ++++ b/data/icons/meson.build +@@ -1 +1,2 @@ + install_subdir('hicolor', install_dir: icondir) ++install_subdir('scalable', install_dir: icondir) +-- +2.51.0 + + +From e0b648ce306e29ecb754711700d0b95aa4ed8769 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Mon, 15 Dec 2025 13:34:27 +0100 +Subject: [PATCH 10/30] gdm/authmenuButton: Use None ornament + +NOT_DOT doesn't exist. +--- + js/gdm/authMenuButton.js | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/js/gdm/authMenuButton.js b/js/gdm/authMenuButton.js +index e58c331b45..9a02b0cb05 100644 +--- a/js/gdm/authMenuButton.js ++++ b/js/gdm/authMenuButton.js +@@ -236,7 +236,7 @@ var AuthMenuButton = GObject.registerClass({ + + _updateOrnament() { + for (const menuItem of this._items.values()) +- menuItem.setOrnament(PopupMenu.Ornament.NO_DOT); ++ menuItem.setOrnament(PopupMenu.Ornament.NONE); + + for (const itemKey of this._activeItems) { + const menuItem = this._getMenuItem(JSON.parse(itemKey)); +-- +2.51.0 + + +From 6a246865fb5a2da8724aafea34995d5aab81c263 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Fri, 12 Dec 2025 20:33:15 +0100 +Subject: [PATCH 11/30] gdm/authMenuButton: Add proper style classes to button + +--- + js/gdm/authMenuButton.js | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/js/gdm/authMenuButton.js b/js/gdm/authMenuButton.js +index 9a02b0cb05..64dcf1a3f4 100644 +--- a/js/gdm/authMenuButton.js ++++ b/js/gdm/authMenuButton.js +@@ -146,7 +146,7 @@ var AuthMenuButton = GObject.registerClass({ + params.sectionOrder ??= []; + super._init({ + ...params, +- style_class: 'login-dialog-button login-dialog-auth-menu-button', ++ style_class: 'modal-dialog-button button login-dialog-auth-menu-button', + reactive: true, + track_hover: true, + can_focus: true, +-- +2.51.0 + + +From 11ad6201e3c032674d4e58f44d9fea9820fb4234 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Fri, 2 Jan 2026 14:48:42 +0100 +Subject: [PATCH 12/30] gdm/authMenuButton: Add style to indicator description + +This is needed to set its color in the lockscreen. +--- + data/theme/gnome-shell-sass/widgets/_login-dialog.scss | 4 ++++ + 1 file changed, 4 insertions(+) + +diff --git a/data/theme/gnome-shell-sass/widgets/_login-dialog.scss b/data/theme/gnome-shell-sass/widgets/_login-dialog.scss +index f3aeed4bf3..29c6803082 100644 +--- a/data/theme/gnome-shell-sass/widgets/_login-dialog.scss ++++ b/data/theme/gnome-shell-sass/widgets/_login-dialog.scss +@@ -123,6 +123,10 @@ $_gdm_fg: $osd_fg_color; + } + } + ++.login-dialog-auth-menu-button-indicator-description { ++ color: $osd_fg_color; ++} ++ + .login-dialog-bottom-button-group { + padding: 32px; + spacing: 16px; +-- +2.51.0 + + +From d7c9b074f8cb0bf2295daa3a518776cae023a039 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Fri, 27 Mar 2026 17:57:03 +0100 +Subject: [PATCH 13/30] gdm/authMenuButton: Add no-op () instead of null + +--- + js/gdm/authMenuButton.js | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/js/gdm/authMenuButton.js b/js/gdm/authMenuButton.js +index 64dcf1a3f4..a211ebe0d8 100644 +--- a/js/gdm/authMenuButton.js ++++ b/js/gdm/authMenuButton.js +@@ -475,7 +475,7 @@ class AuthMenuButtonIndicator extends AuthMenuButton { + this._descriptionLabel, 'visible', + GObject.BindingFlags.SYNC_CREATE, + (bind, source) => [true, !!source], +- null); ++ () => {}); + container.add_child(this._descriptionLabel); + + return container; +-- +2.51.0 + + +From 5fb0f23fda55d433a08db68920ed896cdf9e3d08 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Thu, 2 Apr 2026 19:38:34 +0200 +Subject: [PATCH 14/30] gdm/authMenuButton: Ensure visible of description is + properly binded to + +its text +--- + js/gdm/authMenuButton.js | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/js/gdm/authMenuButton.js b/js/gdm/authMenuButton.js +index a211ebe0d8..3dafd87630 100644 +--- a/js/gdm/authMenuButton.js ++++ b/js/gdm/authMenuButton.js +@@ -510,6 +510,7 @@ class AuthMenuButtonIndicator extends AuthMenuButton { + updateDescriptionLabel() { + const [item] = this._items.size === 1 ? this.getItems() : []; + this._descriptionLabel.text = item?.description ?? ''; ++ this._descriptionLabel.visible = !!this._descriptionLabel.text; + } + + // Override to force visibility even when there's only one item +-- +2.51.0 + + +From e9abaf90b24df1f98108ab48c5d00ec87a6d5c9e Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Fri, 27 Mar 2026 13:38:06 +0100 +Subject: [PATCH 15/30] Fix foreach in wrong type + +Map.keys() returns an iterator which doesn't have .forEach() in older +SpiderMonkey. Wrapped it with [...] to convert to an array first. +--- + js/gdm/authMenuButton.js | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/js/gdm/authMenuButton.js b/js/gdm/authMenuButton.js +index 3dafd87630..393dc5da4b 100644 +--- a/js/gdm/authMenuButton.js ++++ b/js/gdm/authMenuButton.js +@@ -307,7 +307,7 @@ var AuthMenuButton = GObject.registerClass({ + this._items.delete(itemKey); + }); + +- this._sections.keys().forEach(sectionName => { ++ [...this._sections.keys()].forEach(sectionName => { + const itemsInSection = this._findItems({sectionName}); + if (itemsInSection.length === 0) { + const section = this._sections.get(sectionName); +-- +2.51.0 + + +From 2e28cf05ef0033c42c6bb54839184701718bf48a Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Fri, 27 Mar 2026 12:56:24 +0100 +Subject: [PATCH 16/30] Change ??= by if statement + +This old js version doesn't support it. +--- + js/gdm/authMenuButton.js | 6 ++++-- + 1 file changed, 4 insertions(+), 2 deletions(-) + +diff --git a/js/gdm/authMenuButton.js b/js/gdm/authMenuButton.js +index 393dc5da4b..c5e864c686 100644 +--- a/js/gdm/authMenuButton.js ++++ b/js/gdm/authMenuButton.js +@@ -143,7 +143,8 @@ var AuthMenuButton = GObject.registerClass({ + }, + }, class AuthMenuButton extends St.Button { + _init(params) { +- params.sectionOrder ??= []; ++ if (params.sectionOrder === undefined || params.sectionOrder === null) ++ params.sectionOrder = []; + super._init({ + ...params, + style_class: 'modal-dialog-button button login-dialog-auth-menu-button', +@@ -449,7 +450,8 @@ var AuthMenuButton = GObject.registerClass({ + var AuthMenuButtonIndicator = GObject.registerClass( + class AuthMenuButtonIndicator extends AuthMenuButton { + _init(params = {}) { +- params.readOnly ??= true; ++ if (params.readOnly === undefined || params.readOnly === null) ++ params.readOnly = true; + super._init(params); + + this.add_style_class_name('login-dialog-auth-menu-button-indicator'); +-- +2.51.0 + + +From f55cd19afd2028dda3af2bb1490c13ca06caf759 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Fri, 12 Dec 2025 18:26:49 +0100 +Subject: [PATCH 17/30] gdm/util: Don't return a promise in + wait-pending-messages + +The callback of wait-pending-messages was returning a Promise. + +That was causing the error "Could not guess unspecified GValue type". +Avoid it embracing the callback with {} so the return isn't passed. +--- + js/gdm/util.js | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/js/gdm/util.js b/js/gdm/util.js +index 4aeec5b02a..629ba1b629 100644 +--- a/js/gdm/util.js ++++ b/js/gdm/util.js +@@ -531,7 +531,7 @@ var ShellUserVerifier = class { + authServices.connect('queue-priority-message', + (_, ...args) => this._queuePriorityMessage(...args)), + authServices.connect('wait-pending-messages', +- (_, ...args) => this._waitPendingMessages(...args)), ++ (_, ...args) => {this._waitPendingMessages(...args);}), + authServices.connect('filter-messages', + (_, ...args) => this._filterServiceMessages(...args)), + authServices.connect('verification-failed', +-- +2.51.0 + + +From 179858faf126b93d734452ebb522383b07bf0a1d Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Fri, 12 Dec 2025 16:26:57 +0100 +Subject: [PATCH 18/30] gdm/authPrompt: Fixes + +1. Don't set directly icon_name in StButton +2. Guard setting text on entries on clear +3. Use Const instead of GdmUtil to get standard service_names +4. Inputwell was an extra container, in old version directly store in + this. +5. On setChoiceList, don't hide _message. This keeps the same layout + avoiding height increase of mainBox +6. On vfunc_key_press_event, handle event more explicitly instead of + just calling event. +--- + js/gdm/authPrompt.js | 27 ++++++++++++++++++--------- + 1 file changed, 18 insertions(+), 9 deletions(-) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index 6996dac18b..7f683b2ba0 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -146,8 +146,17 @@ var AuthPrompt = GObject.registerClass({ + if (keyPressEvent.keyval == Clutter.KEY_Escape) + this.cancel(); + +- if (this._preemptiveInput && !this._pendingActivate) +- return this._entry.clutter_text.event(keyPressEvent, false); ++ if (this._preemptiveInput && !this._pendingActivate) { ++ const unichar = keyPressEvent.unicode_value; ++ if (keyPressEvent.keyval === Clutter.KEY_Return && ++ this._entry.clutter_text.text) { ++ this._pendingActivate = true; ++ } else if (GLib.unichar_isprint(unichar)) { ++ this._entry.clutter_text.insert_text(unichar, ++ this._entry.clutter_text.cursor_position); ++ } ++ return Clutter.EVENT_STOP; ++ } + + return super.vfunc_key_press_event(keyPressEvent); + } +@@ -200,7 +209,7 @@ var AuthPrompt = GObject.registerClass({ + }, + }); + }); +- this._inputWell.add_child(this._authList); ++ this.add_child(this._authList); + + // Use an insensitive button for the auth list title + // to get the same style as the auth list buttons +@@ -291,7 +300,7 @@ var AuthPrompt = GObject.registerClass({ + button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE, + reactive: true, + can_focus: false, +- icon_name: 'go-next-symbolic', ++ child: new St.Icon({icon_name: 'go-next-symbolic'}), + }); + this._nextButton.connect('clicked', () => this._activateNext()); + this._nextButton.add_style_pseudo_class('default'); +@@ -322,7 +331,7 @@ var AuthPrompt = GObject.registerClass({ + } + }); + this._webLoginDialog.connect('loading', () => this.emit('loading', this._webLoginDialog.isLoading)); +- this._inputWell.add_child(this._webLoginDialog); ++ this.add_child(this._webLoginDialog); + } + + _updateShowPasswordIcon() { +@@ -638,8 +647,10 @@ var AuthPrompt = GObject.registerClass({ + }); + + if (!reuseEntryText) { +- this._entry.text = ''; +- this._inactiveEntry.text = ''; ++ if (this._entry) ++ this._entry.text = ''; ++ if (this._inactiveEntry) ++ this._inactiveEntry.text = ''; + } + + this._entryArea.hide(); +@@ -712,8 +723,6 @@ var AuthPrompt = GObject.registerClass({ + } + + this._entryArea.hide(); +- if (this._message.text === '') +- this._message.hide(); + this._fadeInElement(this._authList); + } + +-- +2.51.0 + + +From bf8ecd9f1dec3a58c9734ae0b6bfb5ab6f7e0e0d Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Fri, 12 Dec 2025 18:48:48 +0100 +Subject: [PATCH 19/30] gdm/authList: Use vertical instead of orientation + +orientation isn't a valid property for St.BoxLayout +--- + js/gdm/authList.js | 6 +++--- + 1 file changed, 3 insertions(+), 3 deletions(-) + +diff --git a/js/gdm/authList.js b/js/gdm/authList.js +index 6eb34f6d97..b5072fe945 100644 +--- a/js/gdm/authList.js ++++ b/js/gdm/authList.js +@@ -32,7 +32,7 @@ const ItemIconPopup = class extends PopupMenu.PopupMenu { + this.actor.add_style_class_name('login-dialog-item-icon-popup'); + + const labels = new St.BoxLayout({ +- orientation: Clutter.Orientation.VERTICAL, ++ vertical: true, + style_class: 'login-dialog-item-icon-popup-labels', + }); + labels.add_child(new St.Label({text: title})); +@@ -95,7 +95,7 @@ const AuthListItem = GObject.registerClass({ + x_expand: true, + }); + this._labelBox = new St.BoxLayout({ +- orientation: Clutter.Orientation.VERTICAL, ++ vertical: true, + y_align: Clutter.ActorAlign.CENTER, + x_expand: true, + }); +@@ -171,7 +171,7 @@ var AuthList = GObject.registerClass({ + }, class AuthList extends St.BoxLayout { + _init() { + super._init({ +- orientation: Clutter.Orientation.VERTICAL, ++ vertical: true, + style_class: 'login-dialog-auth-list-layout', + y_align: Clutter.ActorAlign.CENTER, + x_expand: true, +-- +2.51.0 + + +From 2c10304549a203ea1e5fca2ad6e08e0ee5b0f701 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Fri, 27 Mar 2026 17:47:17 +0100 +Subject: [PATCH 20/30] gdm/authList: Use St.Icon instead of iconName as + property + +--- + js/gdm/authList.js | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/js/gdm/authList.js b/js/gdm/authList.js +index b5072fe945..0c8ef1f18d 100644 +--- a/js/gdm/authList.js ++++ b/js/gdm/authList.js +@@ -62,7 +62,7 @@ class ItemIcon extends St.Button { + _init(iconName, iconTitle, iconSubtitle) { + super._init({ + style_class: 'login-dialog-item-icon', +- iconName, ++ child: new St.Icon({icon_name: iconName}), + }); + + this._popup = new ItemIconPopup(this, iconTitle, iconSubtitle); +-- +2.51.0 + + +From 480c3f08428161c3f5ef2e85d7ddc7e9a0c42737 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Fri, 12 Dec 2025 19:02:04 +0100 +Subject: [PATCH 21/30] gdm/loginDialog: Use propper icon name for + authMenuButton + +--- + js/gdm/loginDialog.js | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js +index f59663401b..ab5b9c3d77 100644 +--- a/js/gdm/loginDialog.js ++++ b/js/gdm/loginDialog.js +@@ -445,7 +445,7 @@ var LoginDialog = GObject.registerClass({ + _createAuthMenuButton() { + this._authMenuButton = new AuthMenuButton.AuthMenuButton({ + title: _('Login Options'), +- iconName: 'cog-wheel-symbolic', ++ iconName: 'emblem-system-symbolic', + sectionOrder: [_PRIMARY_LOGIN_METHOD_SECTION_NAME, _SESSION_TYPE_SECTION_NAME], + }); + this._authMenuButton.updateSensitivity(false); +-- +2.51.0 + + +From 56de4883ea483307607d148e2880da220cf782c5 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Wed, 1 Apr 2026 13:05:30 +0200 +Subject: [PATCH 22/30] gdm/LoginDialog: Reduce min height of authprompt + +authprompt is smaller so min height has to be smaller +--- + js/gdm/loginDialog.js | 2 +- + js/ui/unlockDialog.js | 2 +- + 2 files changed, 2 insertions(+), 2 deletions(-) + +diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js +index ab5b9c3d77..cdf6f23e2d 100644 +--- a/js/gdm/loginDialog.js ++++ b/js/gdm/loginDialog.js +@@ -34,7 +34,7 @@ const UserWidget = imports.ui.userWidget; + + const _FADE_ANIMATION_TIME = 250; + const _SCROLL_ANIMATION_TIME = 500; +-const _FIXED_TOP_ACTOR_HEIGHT = 400; ++const _FIXED_TOP_ACTOR_HEIGHT = 300; + const _TIMED_LOGIN_IDLE_THRESHOLD = 5.0; + const _PRIMARY_LOGIN_METHOD_SECTION_NAME = _('Login Options'); + const _SESSION_TYPE_SECTION_NAME = _('Session Type'); +diff --git a/js/ui/unlockDialog.js b/js/ui/unlockDialog.js +index 654959190e..477edbe8fd 100644 +--- a/js/ui/unlockDialog.js ++++ b/js/ui/unlockDialog.js +@@ -32,7 +32,7 @@ const BLUR_SIGMA = 60; + + const SUMMARY_ICON_SIZE = 32; + +-const FIXED_PROMPT_HEIGHT = 400; ++const FIXED_PROMPT_HEIGHT = 300; + + var NotificationsBox = GObject.registerClass({ + Signals: { 'wake-up-screen': {} }, +-- +2.51.0 + + +From 074089190aad31dc52f4da9748a2bda7cb04f239 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Fri, 12 Dec 2025 16:21:02 +0100 +Subject: [PATCH 23/30] gdm/webLogin: Some fixes for backport + +1. Use connect with notify::reactive because bind_property_full is not available +2. Use vertical property because orientation isn't available +3. Set '' as the default value for message +--- + js/gdm/webLogin.js | 15 +++++++-------- + 1 file changed, 7 insertions(+), 8 deletions(-) + +diff --git a/js/gdm/webLogin.js b/js/gdm/webLogin.js +index f4972144fa..d2e947f646 100644 +--- a/js/gdm/webLogin.js ++++ b/js/gdm/webLogin.js +@@ -144,14 +144,13 @@ var WebLoginDialog = GObject.registerClass({ + y_expand: true, + visible: false, + }); +- this.bind_property_full('reactive', +- this, 'opacity', +- GObject.BindingFlags.SYNC_CREATE, +- (_bind, source) => [true, source ? 255 : 128], +- null); ++ this.connect('notify::reactive', () => { ++ this.opacity = this.reactive ? 255 : 128; ++ }); ++ this.opacity = this.reactive ? 255 : 128; + + this._contentBox = new St.BoxLayout({ +- orientation: Clutter.Orientation.VERTICAL, ++ vertical: true, + x_expand: true, + y_expand: true, + }); +@@ -161,7 +160,7 @@ var WebLoginDialog = GObject.registerClass({ + this._contentBox.add_child(this._webLoginPrompt); + + this._buttonBox = new St.BoxLayout({ +- orientation: Clutter.Orientation.HORIZONTAL, ++ vertical: false, + x_align: Clutter.ActorAlign.CENTER, + x_expand: true, + }); +@@ -290,7 +289,7 @@ var WebLoginIntro = GObject.registerClass( + class WebLoginIntro extends St.Button { + _init(params) { + const {message} = Params.parse(params, { +- message: null, ++ message: '', + }); + + const label = new St.Label({ +-- +2.51.0 + + +From 908cce536d7933bb09138e4022af46813fe82092 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Mon, 15 Dec 2025 13:36:31 +0100 +Subject: [PATCH 24/30] gdm/webLogin: Vertically center align labels of buttons + +--- + js/gdm/webLogin.js | 3 +++ + 1 file changed, 3 insertions(+) + +diff --git a/js/gdm/webLogin.js b/js/gdm/webLogin.js +index d2e947f646..062ad5f309 100644 +--- a/js/gdm/webLogin.js ++++ b/js/gdm/webLogin.js +@@ -205,6 +205,7 @@ var WebLoginDialog = GObject.registerClass({ + child: new St.Label({ + text: b.label, + style_class: 'web-login-button-label', ++ y_align: Clutter.ActorAlign.CENTER, + }), + }); + +@@ -295,6 +296,7 @@ class WebLoginIntro extends St.Button { + const label = new St.Label({ + text: message, + style_class: 'web-login-button-label', ++ y_align: Clutter.ActorAlign.CENTER, + }); + + super._init({ +@@ -304,6 +306,7 @@ class WebLoginIntro extends St.Button { + reactive: true, + can_focus: true, + child: label, ++ y_align: Clutter.ActorAlign.CENTER, + }); + } + +-- +2.51.0 + + +From 0e08e9a99057f24e3f8e57d66115d8368d00f2d0 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Mon, 15 Dec 2025 13:53:08 +0100 +Subject: [PATCH 25/30] gdm/webLogin: Set data to qr texture with the right + args + +This old version has a different signature for setting text data. +--- + js/ui/qrCode.js | 6 ++---- + 1 file changed, 2 insertions(+), 4 deletions(-) + +diff --git a/js/ui/qrCode.js b/js/ui/qrCode.js +index 547ad7d153..adf6e40051 100644 +--- a/js/ui/qrCode.js ++++ b/js/ui/qrCode.js +@@ -87,14 +87,12 @@ var QrCode = GObject.registerClass({ + 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, ++ content.set_data( ++ pixelData.get_data(), + Cogl.PixelFormat.RGBA_8888, + qrSize, + qrSize, +-- +2.51.0 + + +From 97ac6d29ff531f15dcadd907164ae5c4609bb016 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Fri, 26 Dec 2025 12:30:43 +0100 +Subject: [PATCH 26/30] gdm/webLogin: Make spinner size 16 + +In old versions spinner icon would be duplicated to fill the space +instead of scaled. +--- + js/gdm/webLogin.js | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/js/gdm/webLogin.js b/js/gdm/webLogin.js +index 062ad5f309..ac79816699 100644 +--- a/js/gdm/webLogin.js ++++ b/js/gdm/webLogin.js +@@ -12,7 +12,7 @@ const { QrCode } = imports.ui.qrCode; + Gio._promisify(GnomeQR, 'generate_qr_code_async', 'generate_qr_code_finish'); + + const QR_CODE_SIZE = 150; +-const WEB_LOGIN_SPINNER_SIZE = 35; ++const WEB_LOGIN_SPINNER_SIZE = 16; + const URL_LABEL_LONG_THRESHOLD = 45; + + var WebLoginPrompt = GObject.registerClass( +-- +2.51.0 + + +From 36e72660620d28929d250ee958df0eb0834a163d Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Fri, 26 Dec 2025 12:31:48 +0100 +Subject: [PATCH 27/30] gdm/unlockDialog: Use proper icon for gear + +In old versions the icon name is called 'emblem-system-symbolic' +--- + js/ui/unlockDialog.js | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/js/ui/unlockDialog.js b/js/ui/unlockDialog.js +index 477edbe8fd..85b512aa85 100644 +--- a/js/ui/unlockDialog.js ++++ b/js/ui/unlockDialog.js +@@ -646,7 +646,7 @@ var UnlockDialog = GObject.registerClass({ + // Login Options button + this._authMenuButton = new AuthMenuButton.AuthMenuButton({ + title: _('Login Options'), +- iconName: 'cog-wheel-symbolic', ++ iconName: 'emblem-system-symbolic', + }); + this._authMenuButton.connect('active-item-changed', () => { + const authMechanism = this._authMenuButton.getActiveItem(); +-- +2.51.0 + + +From 39d93c9d86f99e3ed46544d052ceae188e2adc25 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Fri, 27 Mar 2026 17:53:33 +0100 +Subject: [PATCH 28/30] gdm/unlockDialog: Use vertical boolean instead + orientation + +--- + js/ui/unlockDialog.js | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/js/ui/unlockDialog.js b/js/ui/unlockDialog.js +index 85b512aa85..ab4141e2c3 100644 +--- a/js/ui/unlockDialog.js ++++ b/js/ui/unlockDialog.js +@@ -335,7 +335,7 @@ class UnlockDialogClock extends St.BoxLayout { + _init() { + super._init({ + style_class: 'unlock-dialog-clock', +- orientation: Clutter.Orientation.VERTICAL, ++ vertical: true, + y_align: Clutter.ActorAlign.CENTER, + }); + +-- +2.51.0 + + +From 0b4f7dcb4ec6821e38888898a8298a1e8a5e88bf Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Thu, 2 Apr 2026 18:55:31 +0200 +Subject: [PATCH 29/30] gdm/unlockDialog: Don't use timeout_add_once + +--- + js/gdm/authServicesLegacy.js | 3 ++- + 1 file changed, 2 insertions(+), 1 deletion(-) + +diff --git a/js/gdm/authServicesLegacy.js b/js/gdm/authServicesLegacy.js +index 5955aeadff..4c5a49effa 100644 +--- a/js/gdm/authServicesLegacy.js ++++ b/js/gdm/authServicesLegacy.js +@@ -136,12 +136,13 @@ var AuthServicesLegacy = GObject.registerClass({ + this._fingerprintReadyTimeoutId !== 0) + return; + +- this._fingerprintReadyTimeoutId = GLib.timeout_add_once( ++ this._fingerprintReadyTimeoutId = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, + FINGERPRINT_READY_TIMEOUT_MS, + () => { + this._fingerprintReadyTimeoutId = 0; + this._setFingerprintReady(true); ++ return GLib.SOURCE_REMOVE; + }); + } + +-- +2.51.0 + + +From 17a21d8e27fddb369977c50e9e62de177222120b Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Fri, 26 Dec 2025 12:34:11 +0100 +Subject: [PATCH 30/30] fingerprintManager: Update getDefaultService using + remote + +This is how async methods are called in this version. +--- + js/misc/fingerprintManager.js | 32 ++++++++++++++++---------------- + 1 file changed, 16 insertions(+), 16 deletions(-) + +diff --git a/js/misc/fingerprintManager.js b/js/misc/fingerprintManager.js +index 151e787e0b..4467e0bfa9 100644 +--- a/js/misc/fingerprintManager.js ++++ b/js/misc/fingerprintManager.js +@@ -64,23 +64,23 @@ var FingerprintManager = GObject.registerClass({ + } + + async checkReaderType(cancellable = null) { +- try { +- // Wrappers don't support null cancellable, so let's ignore it in case +- const args = cancellable ? [cancellable] : []; +- const [devicePath] = +- await this._fingerprintManagerProxy.GetDefaultDeviceAsync(...args); +- +- const fprintDeviceProxy = this._getFprintDeviceProxy(devicePath); +- await fprintDeviceProxy.init_async(GLib.PRIORITY_DEFAULT, cancellable ?? null); +- +- const fingerprintReaderKey = fprintDeviceProxy['scan-type'].toUpperCase(); +- const fingerprintReaderType = FingerprintReaderType[fingerprintReaderKey]; +- this._setFingerprintReaderType(fingerprintReaderType); +- } catch (e) { +- if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) ++ this._fingerprintManagerProxy.GetDefaultDeviceRemote((result, error) => { ++ if (error) { ++ this._handleFingerprintError(error); + return; +- this._handleFingerprintError(e); +- } ++ } ++ ++ try { ++ const [devicePath] = result; ++ const fprintDeviceProxy = this._getFprintDeviceProxy(devicePath); ++ fprintDeviceProxy.init(cancellable ?? null); ++ const fingerprintReaderKey = fprintDeviceProxy['scan-type'].toUpperCase(); ++ const fingerprintReaderType = FingerprintReaderType[fingerprintReaderKey]; ++ this._setFingerprintReaderType(fingerprintReaderType); ++ } catch (e) { ++ this._handleFingerprintError(e); ++ } ++ }); + } + + async _initFingerprintManagerProxy() { +-- +2.51.0 + diff --git a/SOURCES/pre-changes-for-passwordless-gdm-backport.patch b/SOURCES/pre-changes-for-passwordless-gdm-backport.patch new file mode 100644 index 0000000..3a9b58e --- /dev/null +++ b/SOURCES/pre-changes-for-passwordless-gdm-backport.patch @@ -0,0 +1,348 @@ +From 38531053c836ca7ae91e5f20785a67e690422d01 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= +Date: Mon, 25 Aug 2025 14:25:49 +0200 +Subject: [PATCH 1/6] gdm/authPrompt: Unset the entry reference on destruction + +We use the internal entry as the reference to the text entry that is +currently in use, but during destruction we do not unset it, thus the +garbage collector may requires further cycles to actually release it. + +Simplify its job here, improves 572d011894c9b490b8a17ad7db6adec335fbb017 + +Part-of: +--- + js/gdm/authPrompt.js | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index 2b65eee8b9..e4950e56ce 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -133,6 +133,7 @@ var AuthPrompt = GObject.registerClass({ + + this._userVerifier.destroy(); + this._userVerifier = null; ++ this._entry = null; + } + + vfunc_key_press_event(keyPressEvent) { +-- +2.52.0 + + +From 34ff1bdba494f66282cba3c8a955b48b041371cc Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= +Date: Mon, 25 Aug 2025 14:29:55 +0200 +Subject: [PATCH 2/6] gdm/authPrompt: Clear the inactive entry too + +In multi-factor authentication cases we may end up clearing the current +entry, but we are potentially leaving a previously filled inactive entry +as it is. + +Avoid this, and clear all the entries we have when clearing the prompt. + +Part-of: +--- + js/gdm/authPrompt.js | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index e4950e56ce..2d306d16b5 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -489,6 +489,7 @@ var AuthPrompt = GObject.registerClass({ + + clear() { + this._entry.text = ''; ++ this._inactiveEntry.text = ''; + this.stopSpinning(); + this._authList.clear(); + this._authList.hide(); +-- +2.52.0 + + +From 576c74f85fe316ef541a347bd51134ceb091658a Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= +Date: Tue, 26 Aug 2025 16:40:07 +0200 +Subject: [PATCH 3/6] gdm/authPrompt: Factorize the entry update code + +Cleanup the code a bit to make future updates smaller + +Part-of: +--- + js/gdm/authPrompt.js | 17 +++++++++++++---- + 1 file changed, 13 insertions(+), 4 deletions(-) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index 2d306d16b5..ed42409b12 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -305,13 +305,22 @@ var AuthPrompt = GObject.registerClass({ + } + + _updateEntry(secret) { ++ let newEntry, inactiveEntry; ++ + if (secret && this._entry !== this._passwordEntry) { +- this._mainBox.replace_child(this._entry, this._passwordEntry); +- this._entry = this._passwordEntry; ++ newEntry = this._passwordEntry; ++ inactiveEntry = this._textEntry; + } else if (!secret && this._entry !== this._textEntry) { +- this._mainBox.replace_child(this._entry, this._textEntry); +- this._entry = this._textEntry; ++ newEntry = this._textEntry; ++ inactiveEntry = this._passwordEntry; ++ } ++ ++ if (newEntry) { ++ this._mainBox.replace_child(this._entry, newEntry); ++ this._entry = newEntry; ++ this._inactiveEntry = inactiveEntry; + } ++ + this._capsLockWarningLabel.visible = secret; + } + +-- +2.52.0 + + +From 2b9dd08c26da92ed0b2e86356fb26a1b69d02f66 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= +Date: Mon, 25 Aug 2025 14:35:57 +0200 +Subject: [PATCH 4/6] gdm/authPrompt: Preserve the text when switching entry + visibility + +The text visibility of an auth prompt entry may change dynamically at +runtime, in particular when unlocking the screen with the blank screen +or with the screen shield visible this is happening: + - The user starts typing the PAM credential (may be secret or not) + - Under the hood the PAM conversation starts and actually asks user + for input + - The entry text is not cleared because we use what the user has typed + so far as the preemptive secret. + - The entry is adjusted to follow the requested PAM text visibility + settings. + -> This may lead to switching the actual text entry widget in use + +Now, if the first entry that PAM requires is "ECHO ON", like it may +happen in the case in which a visible value is requested, we end up +"clearing" what the user as typed instead of preserving the value as +pre-filled value. + +Causing the user typing something wrong instead. + +To prevent this we need to restore both the text, the cursor position +and the selection bounds (in case the use pressed some key [binding]) +leading to a position or selection change. + +Part-of: +--- + js/gdm/authPrompt.js | 3 +++ + 1 file changed, 3 insertions(+) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index ed42409b12..e4084ad22d 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -319,6 +319,9 @@ var AuthPrompt = GObject.registerClass({ + this._mainBox.replace_child(this._entry, newEntry); + this._entry = newEntry; + this._inactiveEntry = inactiveEntry; ++ ++ const {text, cursorPosition, selectionBound} = inactiveEntry.clutterText; ++ this._entry.clutterText.set({text, cursorPosition, selectionBound}); + } + + this._capsLockWarningLabel.visible = secret; +-- +2.52.0 + + +From 1670d0e80215cd3c934dc1de79997753b9eafdf8 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= +Date: Fri, 22 Aug 2025 15:08:23 +0200 +Subject: [PATCH 5/6] gdm/authPrompt: Fix key focus handling on choice list + +When a choice list widget is prompted in GDM we call updateSensitivity() +but this does not act on the currently visible authentication widget but +rather on the text entry all the times. + +And this causes that when a choice list is shown we always set the key +focus to the (hidden) text entry. + +This implies that when the list is finally shown, moving the arrow keys +leads makes the entry context menu to be shown instead of being able to +navigate through the entries. + +So, make updateSensitivity use the currently visible authorization +widget instead and use it to control the sensitivity of the choice list + +Part-of: +--- + js/gdm/authPrompt.js | 21 ++++++++++++++------- + 1 file changed, 14 insertions(+), 7 deletions(-) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index e4084ad22d..f9205d41dd 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -524,13 +524,13 @@ var AuthPrompt = GObject.registerClass({ + this._authList.set({ + opacity: 0, + visible: true, +- reactive: false, + }); ++ this.updateSensitivity(false); + this._authList.ease({ + opacity: 255, + duration: MESSAGE_FADE_OUT_ANIMATION_TIME, + transition: Clutter.AnimationMode.EASE_OUT_QUAD, +- onComplete: () => (this._authList.reactive = true), ++ onComplete: () => this.updateSensitivity(true), + }); + } + +@@ -606,18 +606,25 @@ var AuthPrompt = GObject.registerClass({ + } + + updateSensitivity(sensitive) { +- if (this._entry.reactive === sensitive) ++ let authWidget; ++ ++ if (this._authList.visible) ++ authWidget = this._authList; ++ else ++ authWidget = this._entry; ++ ++ if (authWidget.reactive === sensitive) + return; + +- this._entry.reactive = sensitive; ++ authWidget.reactive = sensitive; + + if (sensitive) { +- this._entry.grab_key_focus(); ++ authWidget.grab_key_focus(); + } else { + this.grab_key_focus(); + +- if (this._entry === this._passwordEntry) +- this._entry.password_visible = false; ++ if (authWidget === this._passwordEntry) ++ authWidget.password_visible = false; + } + } + +-- +2.52.0 + + +From c82bbfa5eeb3e3e6455f9d168b0463183d8eaf0a Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Tue, 8 Jul 2025 17:42:56 +0200 +Subject: [PATCH 6/6] loginDialog: Move session/a11y buttons into a box + +The intermediate container allows the theme to control the distance +from the edge and spacing between buttons, which is more accessible +to the designers than computing the values from code. + +Part-of: +--- + .../widgets/_login-dialog.scss | 5 ++++ + js/gdm/loginDialog.js | 27 +++++++++++-------- + 2 files changed, 21 insertions(+), 11 deletions(-) + +diff --git a/data/theme/gnome-shell-sass/widgets/_login-dialog.scss b/data/theme/gnome-shell-sass/widgets/_login-dialog.scss +index f68d5de996..307a751628 100644 +--- a/data/theme/gnome-shell-sass/widgets/_login-dialog.scss ++++ b/data/theme/gnome-shell-sass/widgets/_login-dialog.scss +@@ -89,6 +89,11 @@ + } + } + ++.login-dialog-bottom-button-group { ++ padding: 32px; ++ spacing: 16px; ++} ++ + .login-dialog-logo-bin { padding: 24px 0px; } + .login-dialog-banner { color: darken($osd_fg_color,10%); } + .login-dialog-button-box { width: 23em; spacing: 5px; } +diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js +index fbced3e4ab..a218e59d78 100644 +--- a/js/gdm/loginDialog.js ++++ b/js/gdm/loginDialog.js +@@ -559,6 +559,11 @@ var LoginDialog = GObject.registerClass({ + bannerBox.add_child(this._bannerLabel); + this._updateBanner(); + ++ this._bottomButtonGroup = new St.BoxLayout({ ++ style_class: 'login-dialog-bottom-button-group', ++ }); ++ this.add_child(this._bottomButtonGroup); ++ + this._sessionMenuButton = new SessionMenuButton(); + this._sessionMenuButton.connect('session-activated', + (list, sessionId) => { +@@ -566,7 +571,7 @@ var LoginDialog = GObject.registerClass({ + }); + this._sessionMenuButton.opacity = 0; + this._sessionMenuButton.show(); +- this.add_child(this._sessionMenuButton); ++ this._bottomButtonGroup.add_child(this._sessionMenuButton); + + this._logoBin = new St.Widget({ style_class: 'login-dialog-logo-bin', + x_align: Clutter.ActorAlign.CENTER, +@@ -625,17 +630,17 @@ var LoginDialog = GObject.registerClass({ + return actorBox; + } + +- _getSessionMenuButtonAllocation(dialogBox) { ++ _getBottomButtonGroupAllocation(dialogBox) { + let actorBox = new Clutter.ActorBox(); + +- let [, , natWidth, natHeight] = this._sessionMenuButton.get_preferred_size(); ++ const [, , natWidth, natHeight] = this._bottomButtonGroup.get_preferred_size(); + + if (this.get_text_direction() === Clutter.TextDirection.RTL) +- actorBox.x1 = dialogBox.x1 + natWidth; ++ actorBox.x1 = dialogBox.x1; + else +- actorBox.x1 = dialogBox.x2 - (natWidth * 2); ++ actorBox.x1 = dialogBox.x2 - natWidth; + +- actorBox.y1 = dialogBox.y2 - (natHeight * 2); ++ actorBox.y1 = dialogBox.y2 - natHeight; + actorBox.x2 = actorBox.x1 + natWidth; + actorBox.y2 = actorBox.y1 + natHeight; + +@@ -698,9 +703,9 @@ var LoginDialog = GObject.registerClass({ + logoHeight = logoAllocation.y2 - logoAllocation.y1; + } + +- let sessionMenuButtonAllocation = null; +- if (this._sessionMenuButton.visible) +- sessionMenuButtonAllocation = this._getSessionMenuButtonAllocation(dialogBox); ++ let bottomButtonGroupAllocation = null; ++ if (this._bottomButtonGroup.visible) ++ bottomButtonGroupAllocation = this._getBottomButtonGroupAllocation(dialogBox); + + // Then figure out if we're overly constrained and need to + // try a different layout, or if we have what extra space we +@@ -801,8 +806,8 @@ var LoginDialog = GObject.registerClass({ + if (logoAllocation) + this._logoBin.allocate(logoAllocation); + +- if (sessionMenuButtonAllocation) +- this._sessionMenuButton.allocate(sessionMenuButtonAllocation); ++ if (bottomButtonGroupAllocation) ++ this._bottomButtonGroup.allocate(bottomButtonGroupAllocation); + } + + _ensureUserListLoaded() { +-- +2.52.0 + diff --git a/SPECS/gnome-shell.spec b/SPECS/gnome-shell.spec index 0ec1f82..2262106 100644 --- a/SPECS/gnome-shell.spec +++ b/SPECS/gnome-shell.spec @@ -8,7 +8,7 @@ Name: gnome-shell Version: 40.10 -Release: 33%{?dist} +Release: 36%{?dist} Summary: Window management and application launching for GNOME License: GPLv2+ @@ -37,7 +37,13 @@ Patch18: 0001-authPrompt-Disregard-smartcard-status-changes-events.patch Patch19: 0001-loginDialog-Show-session-menu-button-when-in-IN_PROG.patch Patch20: 0001-systemActions-Optionally-allow-restart-shutdown-on-l.patch Patch21: 0001-authPrompt-Connect-disable-show-password-key-with-pa.patch -Patch22: 0001-main-Register-session-with-GDM-on-startup.patch +Patch22: 0001-gdm-util-Early-initialize-all-internal-properties.patch +Patch23: 0001-main-Register-session-with-GDM-on-startup.patch +Patch24: 0001-loginManager-Update-RegisterSession.patch +# Passwordless GDM patch series +Patch25: pre-changes-for-passwordless-gdm-backport.patch +Patch26: 0001-Support-for-web-login-and-unified-auth-mechanism.patch +Patch27: post-changes-for-passwordless-gdm-backport.patch # Misc. Patch30: 0001-panel-add-an-icon-to-the-ActivitiesButton.patch @@ -278,6 +284,8 @@ desktop-file-validate %{buildroot}%{_datadir}/applications/evolution-calendar.de %{_datadir}/dbus-1/interfaces/org.gnome.ShellSearchProvider2.xml %{_datadir}/icons/hicolor/scalable/apps/org.gnome.Shell.Extensions.svg %{_datadir}/icons/hicolor/symbolic/apps/org.gnome.Shell.Extensions-symbolic.svg +%{_datadir}/icons/scalable/status/fingerprint-auth-symbolic.svg +%{_datadir}/icons/scalable/status/vcard-symbolic.svg %{_userunitdir}/org.gnome.Shell-disable-extensions.service %{_userunitdir}/org.gnome.Shell.target %{_userunitdir}/org.gnome.Shell@wayland.service @@ -307,6 +315,19 @@ desktop-file-validate %{buildroot}%{_datadir}/applications/evolution-calendar.de %endif %changelog +* Mon May 25 2026 Joan Torres Lopez - 40.10-36 +- Update fix of registering session + Registering the display is needed too + Resolves: RHEL-178690 + +* Thu Apr 2 2026 Joan Torres Lopez - 40.10-35 +- Backport passwordless GDM feature to RHEL 9 + Resolves: RHEL-169914 + +* Mon Mar 9 2026 Joan Torres Lopez - 40.10-34 +- Fix to automatically start fingerprint when enabled + Resolves: RHEL-4166 + * Tue Nov 18 2025 Joan Torres Lopez - 40.10-33 - Register session with GDM on startup Resolves: RHEL-129287