chromium/ash/webui/common/resources/multidevice_setup/multidevice_setup.js

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

import './button_bar.js';
import './fake_mojo_service.js';
import './multidevice_setup_shared.css.js';
import './password_page.js';
import './setup_succeeded_page.js';
import './start_setup_page.js';
import '//resources/ash/common/cr.m.js';
import '//resources/polymer/v3_0/iron-pages/iron-pages.js';

import {assert} from '//resources/ash/common/assert.js';
import {WebUIListenerBehavior} from '//resources/ash/common/web_ui_listener_behavior.js';
import {Polymer} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {HostDevice} from 'chrome://resources/mojo/chromeos/ash/services/multidevice_setup/public/mojom/multidevice_setup.mojom-webui.js';

import {MojoInterfaceProvider, MojoInterfaceProviderImpl} from './mojo_api.js';
import {getTemplate} from './multidevice_setup.html.js';
import {MultiDeviceSetupDelegate} from './multidevice_setup_delegate.js';

/** @enum {string} */
export const PageName = {
  PASSWORD: 'password-page',
  SUCCESS: 'setup-succeeded-page',
  START: 'start-setup-page',
};

export const MultiDeviceSetup = Polymer({
  _template: getTemplate(),
  is: 'multidevice-setup',

  behaviors: [WebUIListenerBehavior],

  properties: {
    /**
     * Delegate object which performs differently in OOBE vs. non-OOBE mode.
     * @type {!MultiDeviceSetupDelegate}
     */
    delegate: Object,

    /**
     * ID of loadTimeData string to be shown on the forward navigation button.
     * @type {string|undefined}
     */
    forwardButtonTextId: {
      type: String,
      computed: 'getForwardButtonTextId_(visiblePage_)',
      notify: true,
    },

    /** Whether the forward button should be disabled. */
    forwardButtonDisabled: {
      type: Boolean,
      computed: 'shouldForwardButtonBeDisabled_(' +
          'passwordPageForwardButtonDisabled_, visiblePageName)',
      notify: true,
    },

    /**
     * ID of loadTimeData string to be shown on the cancel button.
     * @type {string|undefined}
     */
    cancelButtonTextId: {
      type: String,
      computed: 'getCancelButtonTextId_(visiblePage_)',
      notify: true,
    },

    /**
     * ID of loadTimeData string to be shown on the backward navigation
     * button.
     * @type {string|undefined}
     */
    backwardButtonTextId: {
      type: String,
      computed: 'getBackwardButtonTextId_(visiblePage_)',
      notify: true,
    },

    /**
     * Element name of the currently visible page.
     *
     * @type {!PageName}
     */
    visiblePageName: {
      type: String,
      value: PageName.START,
      notify: true,
    },

    /**
     * DOM Element corresponding to the visible page.
     *
     * @private {!PasswordPageElement|!StartSetupPageElement|
     *           !SetupSucceededPageElement}
     */
    visiblePage_: Object,

    /**
     * Authentication token, which is generated by the password page.
     * @private {string}
     */
    authToken_: {
      type: String,
    },

    /**
     * Array of objects representing all potential MultiDevice hosts.
     *
     * @private {!Array<!HostDevice>}
     */
    devices_: Array,

    /**
     * Unique identifier for the currently selected host device. This uses the
     * device's Instance ID if it is available; otherwise, the device's legacy
     * device ID is used.
     * TODO(crbug.com/40105247): When v1 DeviceSync is turned off, only
     * use Instance ID since all devices are guaranteed to have one.
     *
     * Undefined if the no list of potential hosts has been received from mojo
     * service.
     *
     * @private {string|undefined}
     */
    selectedInstanceIdOrLegacyDeviceId_: String,

    /**
     * Whether the password page reports that the forward button should be
     * disabled. This field is only relevant when the password page is
     * visible.
     * @private {boolean}
     */
    passwordPageForwardButtonDisabled_: Boolean,

    /**
     * Provider of an interface to the MultiDeviceSetup Mojo service.
     * @private {!MojoInterfaceProvider}
     */
    mojoInterfaceProvider_: Object,

    /**
     * Whether a shadow should appear over the button bar; the shadow is
     * intended to appear when the contents are not scrolled to the bottom to
     * indicate that more contents can be viewed below.
     * @private
     */
    isScrolledToBottom_: {
      type: Boolean,
      value: false,
    },
  },

  listeners: {
    'scroll': 'onWindowContentUpdate_',
    'backward-navigation-requested': 'onBackwardNavigationRequested_',
    'cancel-requested': 'onCancelRequested_',
    'forward-navigation-requested': 'onForwardNavigationRequested_',
  },

  /** @override */
  created() {
    this.mojoInterfaceProvider_ = MojoInterfaceProviderImpl.getInstance();
  },

  /** @override */
  ready() {
    this.addWebUIListener(
        'multidevice_setup.initializeSetupFlow',
        this.initializeSetupFlow.bind(this));
  },

  /** @override */
  attached() {
    window.addEventListener(
        'orientationchange', this.onWindowContentUpdate_.bind(this));
    window.addEventListener('resize', this.onWindowContentUpdate_.bind(this));
  },

  /** @override */
  detached() {
    window.removeEventListener(
        'orientationchange', this.onWindowContentUpdate_.bind(this));
    window.removeEventListener(
        'resize', this.onWindowContentUpdate_.bind(this));
  },

  updateLocalizedContent() {
    this.$.ironPages.querySelectorAll('.ui-page')
        .forEach(page => page.i18nUpdateLocale());
    this.$.buttonBar.i18nUpdateLocale();
  },

  initializeSetupFlow() {
    this.$$('start-setup-page').setPlayAnimation(true);
    this.mojoInterfaceProvider_.getMojoServiceRemote()
        .getEligibleActiveHostDevices()
        .then((responseParams) => {
          if (responseParams.eligibleHostDevices.length === 0) {
            console.warn('Potential host list is empty.');
            return;
          }

          this.devices_ = responseParams.eligibleHostDevices;
          this.fire('forward-button-focus-requested');
        })
        .catch((error) => {
          console.warn('Mojo service failure: ' + error);
        });
  },

  /** @private */
  onCancelRequested_() {
    this.exitSetupFlow_(false /* didUserCompleteSetup */);
  },

  /**
   * Called when contents are scrolled, the window is resized, or the window's
   * orientation is updated.
   * @private
   */
  onWindowContentUpdate_() {
    // (scrollHeight - scrollTop) represents the visible height of the
    // contents, not including scrollbars.
    const visibleHeight = this.scrollHeight - this.scrollTop;

    // If these two heights are equal, the contents are scrolled to the
    // bottom. Instead of using equality, we check that the difference is
    // sufficiently small to account for fractional values due to browser
    // zoom and/or display density.
    this.isScrolledToBottom_ = Math.abs(this.clientHeight - visibleHeight) < 1;
  },

  /** @private */
  onBackwardNavigationRequested_() {
    // The back button is only visible on the password page.
    assert(this.visiblePageName === PageName.PASSWORD);

    this.$$('password-page').clearPasswordTextInput();
    this.visiblePageName = PageName.START;
    this.fire('forward-button-focus-requested');
  },

  /** @private */
  onForwardNavigationRequested_() {
    if (this.forwardButtonDisabled) {
      return;
    }

    this.visiblePage_.getCanNavigateToNextPage().then((canNavigate) => {
      if (!canNavigate) {
        return;
      }
      this.navigateForward_();
    });
  },

  /** @private */
  navigateForward_() {
    switch (this.visiblePageName) {
      case PageName.PASSWORD:
        this.$$('password-page').clearPasswordTextInput();
        this.setHostDevice_();
        return;
      case PageName.SUCCESS:
        this.exitSetupFlow_(true /* didUserCompleteSetup */);
        return;
      case PageName.START:
        if (this.delegate.isPasswordRequiredToSetHost()) {
          this.visiblePageName = PageName.PASSWORD;
          this.$$('password-page').focusPasswordTextInput();
        } else {
          this.setHostDevice_();
        }
        return;
    }
  },

  /** @private */
  setHostDevice_() {
    // An authentication token must be set if a password is required.
    assert(this.delegate.isPasswordRequiredToSetHost() === !!this.authToken_);

    const instanceIdOrLegacyDeviceId =
        /** @type {string} */ (this.selectedInstanceIdOrLegacyDeviceId_);
    this.delegate.setHostDevice(instanceIdOrLegacyDeviceId, this.authToken_)
        .then((responseParams) => {
          if (!responseParams.success) {
            console.warn(
                'Failure setting host with ID: ' + instanceIdOrLegacyDeviceId);
            return;
          }

          if (this.delegate.shouldExitSetupFlowAfterSettingHost()) {
            this.exitSetupFlow_(true /* didUserCompleteSetup */);
            return;
          }

          this.visiblePageName = PageName.SUCCESS;
          this.fire('forward-button-focus-requested');
        })
        .catch((error) => {
          console.warn('Mojo service failure: ' + error);
        });
  },

  /** @private */
  onUserSubmittedPassword_() {
    this.onForwardNavigationRequested_();
  },

  /**
   * @return {string|undefined} The ID of loadTimeData string for the
   *     forward button text, which is undefined if no button should be
   *     displayed.
   * @private
   */
  getForwardButtonTextId_() {
    if (!this.visiblePage_) {
      return undefined;
    }
    return this.visiblePage_.forwardButtonTextId;
  },

  /**
   * @return {boolean} Whether the forward button should be disabled.
   * @private
   */
  shouldForwardButtonBeDisabled_() {
    return (this.visiblePageName === PageName.PASSWORD) &&
        this.passwordPageForwardButtonDisabled_;
  },

  /**
   * @return {string|undefined} The ID of loadTimeData string for the
   *     cancel button text, which is undefined if no button should be
   *     displayed.
   * @private
   */
  getCancelButtonTextId_() {
    if (!this.visiblePage_) {
      return undefined;
    }
    return this.visiblePage_.cancelButtonTextId;
  },

  /**
   * @return {string|undefined} The ID of loadTimeData string for the
   *     backward button text, which is undefined if no button should be
   *     displayed.
   * @private
   */
  getBackwardButtonTextId_() {
    if (!this.visiblePage_) {
      return undefined;
    }
    return this.visiblePage_.backwardButtonTextId;
  },

  /**
   * @return {boolean}
   * @private
   */
  shouldPasswordPageBeIncluded_() {
    return this.delegate.isPasswordRequiredToSetHost();
  },

  /**
   * @return {boolean}
   * @private
   */
  shouldSetupSucceededPageBeIncluded_() {
    return !this.delegate.shouldExitSetupFlowAfterSettingHost();
  },

  /**
   * Notifies observers that the setup flow has completed.
   * @param {boolean} didUserCompleteSetup
   * @private
   */
  exitSetupFlow_(didUserCompleteSetup) {
    this.$$('start-setup-page').setPlayAnimation(false);
    this.fire('setup-exited', {didUserCompleteSetup: didUserCompleteSetup});
  },
});