chromium/chrome/browser/resources/ash/settings/multidevice_page/multidevice_screen_lock_subpage.ts

// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * @fileoverview
 * Subpage of settings-multidevice-notification-access-setup-dialog for setting
 * up screen lock.
 */

import 'chrome://resources/ash/common/cr_elements/cr_shared_vars.css.js';
import '../os_people_page/lock_screen_password_prompt_dialog.js';
import '../os_people_page/setup_pin_dialog.js';

import {fireAuthTokenInvalidEvent} from 'chrome://resources/ash/common/quick_unlock/utils.js';
import {assert} from 'chrome://resources/js/assert.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {AuthFactor, AuthFactorConfig, ConfigureResult, FactorObserverReceiver, PinFactorEditor} from 'chrome://resources/mojo/chromeos/ash/services/auth_factor_config/public/mojom/auth_factor_config.mojom-webui.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import {LockScreenUnlockType, LockStateMixin} from '../lock_state_mixin.js';

import {getTemplate} from './multidevice_screen_lock_subpage.html.js';

import TokenInfo = chrome.quickUnlockPrivate.TokenInfo;

const SettingsMultideviceScreenLockSubpageElementBase =
    LockStateMixin(PolymerElement);

export class SettingsMultideviceScreenLockSubpageElement extends
    SettingsMultideviceScreenLockSubpageElementBase {
  static get is() {
    return 'settings-multidevice-screen-lock-subpage' as const;
  }

  static get template() {
    return getTemplate();
  }

  static get properties() {
    return {
      /**
       * Authentication token.
       */
      authTokenInfo_: Object,

      /**
       * True if quick unlock settings are disabled by policy.
       */
      quickUnlockDisabledByPolicy_: {
        type: Boolean,
        value() {
          return loadTimeData.getBoolean('quickUnlockDisabledByPolicy');
        },
        readOnly: true,
      },

      shouldPromptPasswordDialog_: Boolean,

      /** Reflects whether the screen lock is enabled. */
      isScreenLockEnabled: {
        type: Boolean,
        value: false,
        notify: true,
      },

      /** Reflects the password sub-dialog property. */
      isPasswordDialogShowing: {
        type: Boolean,
        value: false,
        notify: true,
      },

      /** Reflects whether the pin dialog should show. */
      showSetupPinDialog: {
        type: Boolean,
        value: false,
        notify: true,
      },

      hasPin: {
        type: Boolean,
        value: false,
      },
    };
  }

  isPasswordDialogShowing: boolean;
  isScreenLockEnabled: boolean;
  showSetupPinDialog: boolean;
  private authTokenInfo_: TokenInfo|undefined;
  private quickUnlockDisabledByPolicy_: boolean;
  private shouldPromptPasswordDialog_: boolean;
  hasPin: boolean;

  static get observers() {
    return [
      'selectedUnlockTypeChanged_(selectedUnlockType)',
      'updatePinState_(authTokenInfo_)',
    ];
  }

  constructor() {
    super();

    if (this.authTokenInfo_ === undefined) {
      this.shouldPromptPasswordDialog_ = true;
    }
  }

  override ready(): void {
    super.ready();

    // Register this object as listener to factor change events (via
    // |onFactorChanged|):
    const receiver = new FactorObserverReceiver(this);
    const remote = receiver.$.bindNewPipeAndPassRemote();
    AuthFactorConfig.getRemote().observeFactorChanges(remote);
  }

  async onFactorChanged(factor: AuthFactor): Promise<void> {
    if (factor !== AuthFactor.kPin) {
      return;
    }
    if (!this.authTokenInfo_) {
      return;
    }

    await this.updatePinState_(this.authTokenInfo_, /*factorChanged=*/ true);
  }

  /**
   * Fetches the state of the PIN factor and updates the corresponding
   * property.
   * @param authTokenInfo Must be equal to `this.authTokenInfo_`. This is
   *     passed as parameter so that this function can be used as callback for
   *     changes of the `authTokenInfo_` property.
   * @param factorChanged Should be `true` if this function is called in
   *     response to a PIN change (as opposed to e.g. during initialization).
   */
  private async updatePinState_(
      authTokenInfo: TokenInfo, factorChanged: boolean = false): Promise<void> {
    if (!authTokenInfo) {
      return;
    }
    const authToken = authTokenInfo.token;
    assert(this.authTokenInfo_ && this.authTokenInfo_.token === authToken);

    const {configured} = await AuthFactorConfig.getRemote().isConfigured(
        authToken, AuthFactor.kPin);
    if (configured) {
      this.hasPin = true;
      this.selectedUnlockType = LockScreenUnlockType.PIN_PASSWORD;
      return;
    }
    assert(!configured);

    // A race condition can occur:
    // (1) User selects PIN_PASSSWORD, and successfully sets a pin, adding
    //     QuickUnlockMode.PIN to active modes.
    // (2) User selects PASSWORD, QuickUnlockMode.PIN capability is cleared
    //     from the active modes. This notifies this class via
    //     |onFactorChanged| and prompts us to fetch the current state of the
    //     PIN asynchronously.
    // (3) User selects PIN_PASSWORD, but the process from step 2 has not yet
    //     completed.
    // In this case, do not forcibly select the PASSWORD radio button even
    // though the unlock type is still PASSWORD (|hasPin| is false). If the
    // user wishes to set a pin, they will have to click the set pin button.
    // See https://crbug.com/1054327 for details.
    if (factorChanged && !this.hasPin &&
        this.selectedUnlockType === LockScreenUnlockType.PIN_PASSWORD) {
      return;
    }
    this.hasPin = false;
    this.selectedUnlockType = LockScreenUnlockType.PASSWORD;
  }

  /**
   * Called when the unlock type has changed.
   * @param selected The current unlock type.
   */
  private async selectedUnlockTypeChanged_(selected: string): Promise<void> {
    const pinNumberEvent = new CustomEvent('pin-number-selected', {
      bubbles: true,
      composed: true,
      detail: {
        isPinNumberSelected: (selected === LockScreenUnlockType.PIN_PASSWORD),
      },
    });
    this.dispatchEvent(pinNumberEvent);
    if (selected === LockScreenUnlockType.PASSWORD && this.authTokenInfo_) {
      // If the user selects PASSWORD only (which sends an asynchronous
      // removePin call to clear the quick unlock capability), indicate to the
      // user immediately that the quick unlock capability is cleared by setting
      // |hasPin| to false. If there is an error clearing quick unlock, revert
      // |hasPin| to true. This prevents setupPinButton UI delays, except in the
      // small chance that CrOS fails to remove the quick unlock capability. See
      // https://crbug.com/1054327 for details.
      this.hasPin = false;
      const {result} = await PinFactorEditor.getRemote().removePin(
          this.authTokenInfo_.token);
      if (result !== ConfigureResult.kSuccess) {
        this.hasPin = true;
      }

      switch (result) {
        case ConfigureResult.kSuccess:
          break;
        case ConfigureResult.kInvalidTokenError:
          fireAuthTokenInvalidEvent(this);
          break;
        case ConfigureResult.kFatalError:
          console.error('Error removing PIN');
          break;
      }
    }
  }

  private onPasswordPromptDialogClose_(): void {
    this.shouldPromptPasswordDialog_ = false;
  }

  private onAuthTokenObtained_(e: CustomEvent<TokenInfo>): void {
    this.authTokenInfo_ = e.detail;
    this.setLockScreenEnabled(
        this.authTokenInfo_.token, true, (_success: boolean) => {});
    this.isScreenLockEnabled = true;
    // Avoid dialog.close() of password_prompt_dialog.ts to close main dialog
    this.isPasswordDialogShowing = true;
  }

  /**
   * Returns true if the setup pin section should be shown.
   * @param selectedUnlockType The current unlock type. Used to let
   *     Polymer know about the dependency.
   */
  private showConfigurePinButton_(selectedUnlockType: string): boolean {
    return selectedUnlockType === LockScreenUnlockType.PIN_PASSWORD;
  }

  private onSetupPinDialogClose_(): void {
    this.showSetupPinDialog = false;
  }
}

declare global {
  interface HTMLElementTagNameMap {
    [SettingsMultideviceScreenLockSubpageElement.is]:
        SettingsMultideviceScreenLockSubpageElement;
  }
}

customElements.define(
    SettingsMultideviceScreenLockSubpageElement.is,
    SettingsMultideviceScreenLockSubpageElement);