[gnome-shell/T29763: 164/249] payg: Add a new screen to allow unlocking PAYG machines



commit ca6825e7916a14fa814acc6fbe3bd68dc9c7efce
Author: Mario Sanchez Prada <mario endlessm com>
Date:   Tue Mar 20 22:16:15 2018 +0000

    payg: Add a new screen to allow unlocking PAYG machines
    
    This will be shown underneath the screenshield when the machine
    has PAYG enabled and no valid code is currently active, right
    before showing the screen to unlock the user's session, if a
    password has been set.
    
    https://phabricator.endlessm.com/T21608
    
     * 2020-03-27:
        + Squash with e55326ddb
        + Squash with 7149ca06a
        + Squash with a2e5c06a9
    
     * 2019-10-21:
        - Remove Mainloop
        - Squash with "0ca7d3a8c payg: Show an explicit error […]"
        - Squash with "720c6c1f6 payg: Use the right strings […]"
        - Squash with "fb4f65c8f payg: Add extra strings and […]"
        - Squash with "7d60ef5e1 paygUnlockDialog: deduplicate […]"

 data/theme/gnome-shell-sass/_endless.scss |  78 +++++
 js/js-resources.gresource.xml             |   1 +
 js/ui/paygUnlockDialog.js                 | 548 ++++++++++++++++++++++++++++++
 js/ui/screenShield.js                     |  78 ++++-
 js/ui/sessionMode.js                      |  12 +-
 po/POTFILES.in                            |   1 +
 6 files changed, 708 insertions(+), 10 deletions(-)
---
diff --git a/data/theme/gnome-shell-sass/_endless.scss b/data/theme/gnome-shell-sass/_endless.scss
index 29f9f0256d..5052564f92 100644
--- a/data/theme/gnome-shell-sass/_endless.scss
+++ b/data/theme/gnome-shell-sass/_endless.scss
@@ -670,3 +670,81 @@ popup-separator-menu-item {
     color: $osd_fg_color;
   }
 }
+
+// Pay-As-You-Go unlock screen (based on .login-dialog)
+
+.unlock-dialog-payg {
+  border: none;
+  background-color: transparent;
+
+  .modal-dialog-button-box { spacing: 3px; }
+  .modal-dialog-button {
+    padding: 3px 18px;
+    &:default {
+      @include button(normal,$c:$selected_bg_color);
+      &:hover,&:focus { @include button(hover,$c:$selected_bg_color); }
+      &:active { @include button(active,$c:$selected_bg_color); }
+      &:insensitive { @include button(insensitive); }
+    }
+  }
+
+  .unlock-dialog-payg-layout {
+      padding-top: 24px;
+      padding-bottom: 12px;
+      spacing: 8px;
+
+      .unlock-dialog-payg-title {
+          color: $osd_fg_color;
+          font-size: 24px;
+          font-weight: bold;
+          text-align: left;
+          margin: 24px;
+      }
+
+      .unlock-dialog-payg-promptbox {
+          spacing: 6px;
+          min-width: 370px;
+          max-width: 370px;
+      }
+
+      .unlock-dialog-payg-label {
+          color: darken($osd_fg_color, 20%);
+          font-size: 110%; // Consistent with .login-dialog-prompt-label
+          padding-top: 1em;
+      }
+
+      .unlock-dialog-payg-entry {
+          font-size: 24px;
+          padding-left: 12px;
+          padding-right: 12px;
+          letter-spacing: 12px;
+      }
+
+      .unlock-dialog-payg-session-list-button {
+          color: darken($osd_fg_color,30%);
+          &:hover,&:focus { color: $osd_fg_color; }
+          &:active { color: darken($osd_fg_color, 50%); }
+      }
+
+      .unlock-dialog-payg-message {
+          color: $warning_color;
+      }
+
+      .unlock-dialog-payg-button-box {
+          spacing: 5px;
+      }
+
+      .unlock-dialog-payg-help-main {
+          color: $osd_fg_color;
+          font-weight: bold;
+          text-align: left;
+          margin-top: 36px;
+      }
+
+      .unlock-dialog-payg-help-sub {
+          color: $osd_fg_color;
+          font-size: 100%;
+          text-align: left;
+      }
+  }
+}
diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml
index 9c284a75ef..f73f31836e 100644
--- a/js/js-resources.gresource.xml
+++ b/js/js-resources.gresource.xml
@@ -153,6 +153,7 @@
     <file>ui/hotCorner.js</file>
     <file>ui/iconGridLayout.js</file>
     <file>ui/internetSearch.js</file>
+    <file>ui/paygUnlockDialog.js</file>
     <file>ui/sideComponent.js</file>
     <file>ui/status/automaticUpdates.js</file>
     <file>ui/userMenu.js</file>
diff --git a/js/ui/paygUnlockDialog.js b/js/ui/paygUnlockDialog.js
new file mode 100644
index 0000000000..f4704b9d4f
--- /dev/null
+++ b/js/ui/paygUnlockDialog.js
@@ -0,0 +1,548 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+//
+// Copyright (C) 2018 Endless Mobile, Inc.
+//
+// Licensed under the GNU General Public License Version 2
+//
+// 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
+// of the License, 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, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+/* exported PaygUnlockDialog */
+
+const { Atk, Clutter, Gio, GLib,
+    GObject, Meta, Pango, Shell, St }  = imports.gi;
+const Gettext = imports.gettext;
+
+const PaygManager = imports.misc.paygManager;
+
+const Animation = imports.ui.animation;
+const Main = imports.ui.main;
+const LayoutManager = imports.ui.layout;
+
+const MSEC_PER_SEC = 1000;
+
+// The timeout before going back automatically to the lock screen
+const IDLE_TIMEOUT_SECS = 2 * 60;
+
+const SPINNER_ICON_SIZE_PIXELS = 16;
+
+var UnlockStatus = {
+    NOT_VERIFYING: 0,
+    VERIFYING: 1,
+    FAILED: 2,
+    TOO_MANY_ATTEMPTS: 3,
+    SUCCEEDED: 4,
+};
+
+var PaygUnlockCodeEntry = GObject.registerClass({
+    Signals: {
+        'code-changed': { param_types: [GObject.TYPE_STRING] },
+    },
+}, class PaygUnlockCodeEntry extends St.Entry {
+
+    _init() {
+        super._init({
+            style_class: 'unlock-dialog-payg-entry',
+            reactive: true,
+            can_focus: true,
+            x_align: Clutter.ActorAlign.FILL,
+        });
+
+        this._code = '';
+        this.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
+        this.clutter_text.x_align = Clutter.ActorAlign.CENTER;
+
+        this._enabled = false;
+        this._buttonPressEventId = this.connect('button-press-event', this._onButtonPressEvent.bind(this));
+        this._capturedEventId = this.clutter_text.connect('captured-event', 
this._onCapturedEvent.bind(this));
+        this._textChangedId = this.clutter_text.connect('text-changed', this._onTextChanged.bind(this));
+
+        this.connect('destroy', this._onDestroy.bind(this));
+    }
+
+    _onDestroy() {
+        if (this._buttonPressEventId > 0) {
+            this.disconnect(this._buttonPressEventId);
+            this._buttonPressEventId = 0;
+        }
+
+        if (this._capturedEventId > 0) {
+            this.clutter_text.disconnect(this._capturedEventId);
+            this._capturedEventId = 0;
+        }
+
+        if (this._textChangedId > 0) {
+            this.clutter_text.disconnect(this._textChangedId);
+            this._textChangedId = 0;
+        }
+    }
+
+    _onCapturedEvent(textActor, event) {
+        if (event.type() !== Clutter.EventType.KEY_PRESS)
+            return Clutter.EVENT_PROPAGATE;
+
+        let keysym = event.get_key_symbol();
+        let isDeleteKey =
+            keysym === Clutter.KEY_Delete ||
+            keysym === Clutter.KEY_KP_Delete ||
+            keysym === Clutter.KEY_BackSpace;
+        let isEnterKey =
+            keysym === Clutter.KEY_Return ||
+            keysym === Clutter.KEY_KP_Enter ||
+            keysym === Clutter.KEY_ISO_Enter;
+        let isExitKey =
+            keysym === Clutter.KEY_Escape ||
+            keysym === Clutter.KEY_Tab;
+        let isMovementKey =
+            keysym === Clutter.KEY_Left ||
+            keysym === Clutter.KEY_Right ||
+            keysym === Clutter.KEY_Home ||
+            keysym === Clutter.KEY_KP_Home ||
+            keysym === Clutter.KEY_End ||
+            keysym === Clutter.KEY_KP_End;
+
+        // Make sure we can leave the entry and delete and
+        // navigate numbers with the keyboard.
+        if (isExitKey || isEnterKey || isDeleteKey || isMovementKey)
+            return Clutter.EVENT_PROPAGATE;
+
+        let character = event.get_key_unicode();
+        this.addCharacter(character);
+
+        return Clutter.EVENT_STOP;
+    }
+
+    _onTextChanged(textActor) {
+        this._code = textActor.text;
+        this.emit('code-changed', this._code);
+    }
+
+    _onButtonPressEvent() {
+        if (!this._enabled)
+            return;
+
+        this.grab_key_focus();
+        return false;
+    }
+
+    addCharacter(character) {
+        if (!this._enabled || !GLib.unichar_isprint(character))
+            return;
+
+        let pos = this.clutter_text.get_cursor_position();
+        let before = pos === -1 ? this._code : this._code.slice(0, pos);
+        let after = pos === -1 ? '' : this._code.slice(pos);
+        let newCode = before + character + after;
+
+        if (!Main.paygManager.validateCode(newCode, true))
+            return;
+
+        this.clutter_text.insert_unichar(character);
+    }
+
+    setEnabled(value) {
+        if (this._enabled === value)
+            return;
+
+        this._enabled = value;
+        this.reactive = value;
+        this.can_focus = value;
+        this.clutter_text.reactive = value;
+        this.clutter_text.editable = value;
+        this.clutter_text.cursor_visible = value;
+    }
+
+    reset() {
+        this.text = '';
+    }
+
+    get code() {
+        return this._code;
+    }
+
+    get length() {
+        return this._code.length;
+    }
+});
+
+var PaygUnlockDialog = GObject.registerClass({
+    Signals: {
+        'failed': {},
+        'wake-up-screen': {},
+    },
+}, class PaygUnlockDialog extends St.Widget {
+
+    _init(parentActor) {
+        super._init({
+            accessible_role: Atk.Role.WINDOW,
+            style_class: 'unlock-dialog-payg',
+            layout_manager: new Clutter.BoxLayout(),
+            visible: false,
+        });
+
+        this._parentActor = parentActor;
+
+        this._entry = null;
+        this._errorMessage = null;
+        this._cancelButton = null;
+        this._nextButton = null;
+        this._spinner = null;
+        this._cancelled = false;
+
+        this._verificationStatus = UnlockStatus.NOT_VERIFYING;
+
+        // Clear the clipboard to make sure nothing can be copied into the entry.
+        St.Clipboard.get_default().set_text(St.ClipboardType.CLIPBOARD, '');
+        St.Clipboard.get_default().set_text(St.ClipboardType.PRIMARY, '');
+
+        this.add_constraint(new LayoutManager.MonitorConstraint({ primary: true }));
+
+        this._parentActor.add_child(this);
+
+        let mainBox = new St.BoxLayout({
+            vertical: true,
+            x_align: Clutter.ActorAlign.FILL,
+            y_align: Clutter.ActorAlign.CENTER,
+            x_expand: true,
+            y_expand: true,
+            style_class: 'unlock-dialog-payg-layout',
+        });
+        this.add_child(mainBox);
+
+        let titleLabel = new St.Label({
+            style_class: 'unlock-dialog-payg-title',
+            text: _('Your Endless pay-as-you-go usage credit has expired.'),
+            x_align: Clutter.ActorAlign.CENTER,
+        });
+        mainBox.add_child(titleLabel);
+
+        let promptBox = new St.BoxLayout({
+            vertical: true,
+            x_align: Clutter.ActorAlign.CENTER,
+            y_align: Clutter.ActorAlign.CENTER,
+            x_expand: true,
+            y_expand: true,
+            style_class: 'unlock-dialog-payg-promptbox',
+        });
+        promptBox.connect('key-press-event', (actor, event) => {
+            if (event.get_key_symbol() === Clutter.KEY_Escape)
+                this._onCancelled();
+
+            return Clutter.EVENT_PROPAGATE;
+        });
+        mainBox.add_child(promptBox);
+
+        let promptLabel = new St.Label({
+            style_class: 'unlock-dialog-payg-label',
+            text: _('Enter a new code to unlock your computer:'),
+            x_align: Clutter.ActorAlign.START,
+        });
+        promptLabel.clutter_text.line_wrap = true;
+        promptLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
+        promptBox.add_child(promptLabel);
+
+        this._entry = new PaygUnlockCodeEntry();
+        promptBox.add_child(this._entry);
+
+        this._errorMessage = new St.Label({
+            opacity: 0,
+            styleClass: 'unlock-dialog-payg-message',
+        });
+        this._errorMessage.clutter_text.line_wrap = true;
+        this._errorMessage.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
+        promptBox.add_child(this._errorMessage);
+
+        this._buttonBox = this._createButtonsArea();
+        promptBox.add_child(this._buttonBox);
+
+        let helpLineMain = new St.Label({
+            style_class: 'unlock-dialog-payg-help-main',
+            text: _('Don’t have an unlock code? That’s OK!'),
+            x_align: Clutter.ActorAlign.START,
+        });
+        helpLineMain.clutter_text.line_wrap = true;
+        helpLineMain.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
+
+        promptBox.add_child(helpLineMain);
+
+        let helpLineSub = new St.Label({
+            style_class: 'unlock-dialog-payg-help-sub',
+            text: _('Talk to your sales representative to purchase a new code.'),
+            x_align: Clutter.ActorAlign.START,
+        });
+        helpLineSub.clutter_text.line_wrap = true;
+        helpLineSub.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
+
+        promptBox.add_child(helpLineSub);
+
+        Main.ctrlAltTabManager.addGroup(promptBox, _('Unlock Machine'), 'dialog-password-symbolic');
+
+        this._cancelButton.connect('clicked', () => {
+            this._onCancelled();
+        });
+        this._nextButton.connect('clicked', () => {
+            this._startVerifyingCode();
+        });
+
+        this._entry.connect('code-changed', () => {
+            this._updateNextButtonSensitivity();
+        });
+
+        this._entry.clutter_text.connect('activate', () => {
+            this._startVerifyingCode();
+        });
+
+        this._clearTooManyAttemptsId = 0;
+        this.connect('destroy', this._onDestroy.bind(this));
+
+        this._idleMonitor = Meta.IdleMonitor.get_core();
+        this._idleWatchId = this._idleMonitor.add_idle_watch(IDLE_TIMEOUT_SECS * MSEC_PER_SEC, 
this._onCancelled.bind(this));
+
+        this._updateSensitivity();
+        this._entry.grab_key_focus();
+    }
+
+    _onDestroy() {
+        this.popModal();
+
+        if (this._idleWatchId) {
+            this._idleMonitor.remove_watch(this._idleWatchId);
+            this._idleWatchId = 0;
+        }
+
+        if (this._clearTooManyAttemptsId > 0) {
+            GLib.source_remove(this._clearTooManyAttemptsId);
+            this._clearTooManyAttemptsId = 0;
+        }
+    }
+
+    _createButtonsArea() {
+        let buttonsBox = new St.BoxLayout({
+            style_class: 'unlock-dialog-payg-button-box',
+            vertical: false,
+            x_expand: true,
+            x_align: Clutter.ActorAlign.FILL,
+            y_expand: true,
+            y_align: Clutter.ActorAlign.END,
+        });
+
+        this._cancelButton = new St.Button({
+            style_class: 'modal-dialog-button button',
+            button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE,
+            reactive: true,
+            can_focus: true,
+            label: _('Cancel'),
+            x_align: St.Align.START,
+            y_align: St.Align.END,
+        });
+        buttonsBox.add_child(this._cancelButton);
+
+        let buttonSpacer = new St.Widget({
+            layout_manager: new Clutter.BinLayout(),
+            x_expand: true,
+            x_align: Clutter.ActorAlign.END,
+        });
+        buttonsBox.add_child(buttonSpacer);
+
+        // We make the most of the spacer to show the spinner while verifying the code.
+        this._spinner = new Animation.Spinner(SPINNER_ICON_SIZE_PIXELS, {
+            animate: true,
+            hideOnStop: true,
+        });
+        buttonSpacer.add_child(this._spinner);
+
+        this._nextButton = new St.Button({
+            style_class: 'modal-dialog-button button',
+            button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE,
+            reactive: true,
+            can_focus: true,
+            label: _('Unlock'),
+            x_align: St.Align.END,
+            y_align: St.Align.END,
+        });
+        this._nextButton.add_style_pseudo_class('default');
+        buttonsBox.add_child(this._nextButton);
+
+        return buttonsBox;
+    }
+
+    _onCancelled() {
+        this._cancelled = true;
+        this._reset();
+
+        // The ScreenShield will connect to the 'failed' signal
+        // to know when to cancel the unlock dialog.
+        if (this._verificationStatus !== UnlockStatus.SUCCEEDED)
+            this.emit('failed');
+    }
+
+    _validateCurrentCode() {
+        return Main.paygManager.validateCode(this._entry.code);
+    }
+
+    _updateNextButtonSensitivity() {
+        let sensitive = this._validateCurrentCode() &&
+            this._verificationStatus !== UnlockStatus.VERIFYING &&
+            this._verificationStatus !== UnlockStatus.TOO_MANY_ATTEMPTS;
+
+        this._nextButton.reactive = sensitive;
+        this._nextButton.can_focus = sensitive;
+    }
+
+    _updateSensitivity() {
+        let shouldEnableEntry = this._verificationStatus !== UnlockStatus.VERIFYING &&
+            this._verificationStatus !== UnlockStatus.TOO_MANY_ATTEMPTS;
+
+        this._updateNextButtonSensitivity();
+        this._entry.setEnabled(shouldEnableEntry);
+    }
+
+    _setErrorMessage(message) {
+        if (message) {
+            this._errorMessage.text = message;
+            this._errorMessage.opacity = 255;
+        } else {
+            this._errorMessage.text = '';
+            this._errorMessage.opacity = 0;
+        }
+    }
+
+    _reset() {
+        this._spinner.stop();
+        this._entry.reset();
+        this._updateSensitivity();
+    }
+
+    _processError(error) {
+        logError(error, 'Error adding PAYG code');
+
+        // The 'too many errors' case is a bit special, and sets a different state.
+        if (error.matches(PaygManager.PaygErrorDomain, PaygManager.PaygError.TOO_MANY_ATTEMPTS)) {
+            let currentTime = GLib.get_real_time() / GLib.USEC_PER_SEC;
+            let secondsLeft = Main.paygManager.rateLimitEndTime - currentTime;
+            if (secondsLeft > 30) {
+                let minutesLeft = Math.max(0, Math.ceil(secondsLeft / 60));
+                this._setErrorMessage(
+                    Gettext.ngettext(
+                        'Too many attempts. Try again in %s minute.',
+                        'Too many attempts. Try again in %s minutes.', minutesLeft).format(minutesLeft));
+            } else {
+                this._setErrorMessage(_('Too many attempts. Try again in a few seconds.'));
+            }
+
+            // Make sure to clean the status once the time is up (if this dialog is still alive)
+            // and make sure that we install this callback at some point in the future (+1 sec).
+            this._clearTooManyAttemptsId = GLib.timeout_add_seconds(
+                GLib.PRIORITY_DEFAULT,
+                Math.max(1, secondsLeft),
+                () => {
+                    this._verificationStatus = UnlockStatus.NOT_VERIFYING;
+                    this._clearError();
+                    this._updateSensitivity();
+                    this._entry.grab_key_focus();
+                    return GLib.SOURCE_REMOVE;
+                });
+
+            this._verificationStatus = UnlockStatus.TOO_MANY_ATTEMPTS;
+            return;
+        }
+
+        // Common errors after this point.
+        if (error.matches(PaygManager.PaygErrorDomain, PaygManager.PaygError.INVALID_CODE)) {
+            this._setErrorMessage(_('Invalid code. Please try again.'));
+        } else if (error.matches(PaygManager.PaygErrorDomain, PaygManager.PaygError.CODE_ALREADY_USED)) {
+            this._setErrorMessage(_('Code already used. Please enter a new code.'));
+        } else if (error.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.TIMED_OUT)) {
+            this._setErrorMessage(_('Time exceeded while verifying the code'));
+        } else {
+            // We don't consider any other error here (and we don't consider DISABLED explicitly,
+            // since that should not happen), but still we need to show something to the user.
+            this._setErrorMessage(_('Unknown error'));
+        }
+
+        this._verificationStatus = UnlockStatus.FAILED;
+    }
+
+    _clearError() {
+        this._setErrorMessage(null);
+    }
+
+    _addCodeCallback(error) {
+        // We don't care about the result if we're closing the dialog.
+        if (this._cancelled) {
+            this._verificationStatus = UnlockStatus.NOT_VERIFYING;
+            return;
+        }
+
+        if (error) {
+            this._processError(error);
+        } else {
+            this._verificationStatus = UnlockStatus.SUCCEEDED;
+            this._clearError();
+        }
+
+        this._reset();
+    }
+
+    _startVerifyingCode() {
+        if (!this._validateCurrentCode())
+            return;
+
+        this._verificationStatus = UnlockStatus.VERIFYING;
+        this._spinner.play();
+        this._updateSensitivity();
+        this._cancelled = false;
+
+        Main.paygManager.addCode(this._entry.code, this._addCodeCallback.bind(this));
+    }
+
+    addCharacter(unichar) {
+        this._entry.addCharacter(unichar);
+    }
+
+    cancel() {
+        this._reset();
+        this.destroy();
+    }
+
+    finish(onComplete) {
+        // Nothing to do other than calling the callback.
+        if (onComplete)
+            onComplete();
+    }
+
+    open(timestamp) {
+        this.show();
+
+        if (this._isModal)
+            return true;
+
+        if (!Main.pushModal(this, {
+            timestamp,
+            actionMode: Shell.ActionMode.UNLOCK_SCREEN,
+        }))
+            return false;
+
+        this._isModal = true;
+
+        return true;
+    }
+
+    popModal(timestamp) {
+        if (this._isModal) {
+            Main.popModal(this, timestamp);
+            this._isModal = false;
+        }
+    }
+});
diff --git a/js/ui/screenShield.js b/js/ui/screenShield.js
index de43d3da19..50aaeb263b 100644
--- a/js/ui/screenShield.js
+++ b/js/ui/screenShield.js
@@ -142,6 +142,40 @@ var ScreenShield = class {
         this._cursorTracker = Meta.CursorTracker.get_for_display(global.display);
 
         this._syncInhibitor();
+
+        Main.paygManager.connect('code-expired', () => { this.lock(true) });
+        Main.paygManager.connect('expiry-time-changed', () => {
+            let userManager = AccountsService.UserManager.get_default();
+            let user = userManager.get_user(GLib.get_user_name());
+
+            // A new valid code has been introduced but the machine is
+            // not unlocked yet -> Make sure we stay in the locked state.
+            if (Main.paygManager.isLocked) {
+                this.lock(false);
+                return;
+            }
+
+            // A new valid code unlocked the machine and user has no
+            // password set -> Go straight to the user's session.
+            if (user.password_mode == AccountsService.UserPasswordMode.NONE) {
+                this.deactivate(false);
+                return;
+            }
+
+            if (Main.sessionMode.currentMode === 'unlock-dialog-payg') {
+                // This is the most common case.
+                Main.sessionMode.popMode('unlock-dialog-payg');
+            }
+
+            // The machine is unlocked but we still need to unlock the
+            // user's session with the password, so don't deactivate yet.
+            if (this._dialog) {
+                this._dialog.destroy();
+                this._dialog = null;
+            }
+
+            this.showDialog();
+        });
     }
 
     _setActive(active) {
@@ -171,7 +205,7 @@ var ScreenShield = class {
             return;
 
         this._dialog.cancel();
-        if (this._isGreeter) {
+        if (this._isGreeter && !Main.paygManager.isLocked) {
             // LoginDialog.cancel() will grab the key focus
             // on its own, so ensure it stays on lock screen
             // instead
@@ -495,6 +529,8 @@ var ScreenShield = class {
     _continueDeactivate(animate) {
         this._hideLockScreen(animate);
 
+        if (Main.sessionMode.currentMode === 'unlock-dialog-payg')
+            Main.sessionMode.popMode('unlock-dialog-payg');
         if (Main.sessionMode.currentMode == 'unlock-dialog')
             Main.sessionMode.popMode('unlock-dialog');
 
@@ -552,6 +588,10 @@ var ScreenShield = class {
         this._isLocked = false;
         this.emit('locked-changed');
         global.set_runtime_state(LOCKED_STATE_STR, null);
+
+        // Sanity check, in case we made it this far while being locked
+        if (Main.paygManager.isLocked)
+            this.lock(false);
     }
 
     activate(animate) {
@@ -562,10 +602,19 @@ var ScreenShield = class {
 
         this.actor.show();
 
-        if (Main.sessionMode.currentMode !== 'unlock-dialog') {
+        if (Main.sessionMode.currentMode !== 'unlock-dialog' &&
+            Main.sessionMode.currentMode !== 'unlock-dialog-payg') {
             this._isGreeter = Main.sessionMode.isGreeter;
-            if (!this._isGreeter)
-                Main.sessionMode.pushMode('unlock-dialog');
+            if (!this._isGreeter) {
+                let userManager = AccountsService.UserManager.get_default();
+                let user = userManager.get_user(GLib.get_user_name());
+
+                if (user.password_mode !== AccountsService.UserPasswordMode.NONE)
+                    Main.sessionMode.pushMode('unlock-dialog');
+
+                if (Main.paygManager.isLocked)
+                    Main.sessionMode.pushMode('unlock-dialog-payg');
+            }
         }
 
         this._resetLockScreen({ animateLockScreen: animate,
@@ -588,7 +637,11 @@ var ScreenShield = class {
     }
 
     lock(animate) {
-        if (this._lockSettings.get_boolean(DISABLE_LOCK_KEY)) {
+        // This does not make sense outside of the user's session for PAYG
+        if (Main.sessionMode.isGreeter && Main.paygManager.isLocked)
+            return;
+
+        if (this._lockSettings.get_boolean(DISABLE_LOCK_KEY) && !Main.paygManager.isLocked) {
             log('Screen lock is locked down, not locking'); // lock, lock - who's there?
             return;
         }
@@ -612,7 +665,7 @@ var ScreenShield = class {
         if (this._isGreeter)
             this._isLocked = true;
         else
-            this._isLocked = user.password_mode != AccountsService.UserPasswordMode.NONE;
+            this._isLocked = Main.paygManager.isLocked || (user.password_mode != 
AccountsService.UserPasswordMode.NONE);
 
         this.activate(animate);
 
@@ -621,10 +674,17 @@ var ScreenShield = class {
 
     // If the previous shell crashed, and gnome-session restarted us, then re-lock
     lockIfWasLocked() {
-        if (!this._settings.get_boolean(LOCK_ENABLED_KEY))
+        // We need to add some extra checks for PAYG becasue we don't want to
+        // end up loging the screen for not regular sessions (e.g. initial-setup).
+        let shouldLockForPayg = Main.paygManager.isLocked &&
+            (Main.sessionMode.currentMode == 'user' ||
+             Main.sessionMode.currentMode == 'user-coding');
+
+        if (!this._settings.get_boolean(LOCK_ENABLED_KEY) && !shouldLockForPayg)
             return;
-        let wasLocked = global.get_runtime_state('b', LOCKED_STATE_STR);
-        if (wasLocked === null)
+
+        let wasLocked = global.get_runtime_state('b',LOCKED_STATE_STR);
+        if (wasLocked === null && !shouldLockForPayg)
             return;
         Meta.later_add(Meta.LaterType.BEFORE_REDRAW, () => {
             this.lock(false);
diff --git a/js/ui/sessionMode.js b/js/ui/sessionMode.js
index e7d1d8cbb6..bbd6ecf3ed 100644
--- a/js/ui/sessionMode.js
+++ b/js/ui/sessionMode.js
@@ -56,7 +56,7 @@ const _modes = {
 
     'unlock-dialog': {
         isLocked: true,
-        unlockDialog: undefined,
+        unlockDialog: imports.ui.unlockDialog.UnlockDialog,
         components: ['polkitAgent', 'telepathyClient'],
         panel: {
             left: [],
@@ -66,6 +66,16 @@ const _modes = {
         panelStyle: 'unlock-screen',
     },
 
+    'unlock-dialog-payg': {
+        parentMode: 'unlock-dialog',
+        unlockDialog: imports.ui.paygUnlockDialog.PaygUnlockDialog,
+        panel: {
+            left: [],
+            center: [],
+            right: ['a11y', 'keyboard', 'aggregateMenu', 'powerMenu'],
+        },
+    },
+
     'user': {
         hasOverview: true,
         showCalendarEvents: true,
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 715792cc44..05cffc4611 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -50,6 +50,7 @@ js/ui/overviewControls.js
 js/ui/overview.js
 js/ui/padOsd.js
 js/ui/panel.js
+js/ui/paygUnlockDialog.js
 js/ui/popupMenu.js
 js/ui/runDialog.js
 js/ui/screenShield.js


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]