blob: 1de30961726b7335e379df11dc2d29ed96acb21c [file] [log] [blame]
// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'chrome://resources/polymer/v3_0/paper-spinner/paper-spinner-lite.js';
import 'chrome://resources/ash/common/cr_elements/cr_button/cr_button.js';
import 'chrome://resources/ash/common/cr_elements/icons.html.js';
import 'chrome://resources/polymer/v3_0/iron-icon/iron-icon.js';
import 'chrome://resources/ash/common/cr_elements/cr_view_manager/cr_view_manager.js';
import 'chrome://chrome-signin/arc_account_picker/arc_account_picker_app.js';
import 'chrome://chrome-signin/gaia_action_buttons/gaia_action_buttons.js';
import './signin_blocked_by_policy_page.js';
import './signin_error_page.js';
import './welcome_page_app.js';
import '/strings.m.js';
import {getAccountAdditionOptionsFromJSON} from 'chrome://chrome-signin/arc_account_picker/arc_util.js';
import type {AuthCompletedCredentials, AuthParams} from 'chrome://chrome-signin/gaia_auth_host/authenticator.js';
import {Authenticator} from 'chrome://chrome-signin/gaia_auth_host/authenticator.js';
import type {CrViewManagerElement} from 'chrome://resources/ash/common/cr_elements/cr_view_manager/cr_view_manager.js';
import {I18nMixin} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js';
import {WebUiListenerMixin} from 'chrome://resources/ash/common/cr_elements/web_ui_listener_mixin.js';
import {assert} from 'chrome://resources/js/assert.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {isRTL} from 'chrome://resources/js/util.js';
import type {PaperSpinnerLiteElement} from 'chrome://resources/polymer/v3_0/paper-spinner/paper-spinner-lite.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {getTemplate} from './inline_login_app.html.js';
import type {InlineLoginBrowserProxy} from './inline_login_browser_proxy.js';
import {InlineLoginBrowserProxyImpl} from './inline_login_browser_proxy.js';
/**
* @fileoverview Inline login WebUI in various signin flows for ChromeOS and
* Chrome desktop (Windows only).
*/
export enum View {
ADD_ACCOUNT = 'addAccount',
ARC_ACCOUNT_PICKER = 'arcAccountPicker',
SIGNIN_BLOCKED_BY_POLICY = 'signinBlockedByPolicy',
SIGNIN_ERROR = 'signinError',
WELCOME = 'welcome',
}
interface NewWindowProperties {
targetUrl: string;
window: {
discard(): void,
};
}
interface WebViewElement extends HTMLElement {
canGoBack(): boolean;
back(): void;
}
interface SigninErrorPageData {
email: string;
hostedDomain: string;
signinBlockedByPolicy: boolean;
deviceType: string;
}
export interface InlineLoginAppElement {
$: {
signinFrame: WebViewElement,
spinner: PaperSpinnerLiteElement,
viewManager: CrViewManagerElement,
};
}
const InlineLoginAppElementBase = WebUiListenerMixin(I18nMixin(PolymerElement));
export class InlineLoginAppElement extends InlineLoginAppElementBase {
static get is() {
return 'inline-login-app';
}
static get template() {
return getTemplate();
}
static get properties() {
return {
/** Mirroring the enum so that it can be used from HTML bindings. */
viewEnum_: {
type: Object,
value: View,
},
/**
* Indicates whether the page is loading.
*/
loading_: {
type: Boolean,
value: true,
},
/**
* Indicates whether the account is being verified.
*/
verifyingAccount_: {
type: Boolean,
value: false,
},
/**
* The auth extension host instance.
*/
authenticator_: {
type: Object,
value: null,
},
/*
* True if welcome page should not be shown.
*/
shouldSkipWelcomePage_: {
type: Boolean,
value() {
return loadTimeData.getBoolean('shouldSkipWelcomePage');
},
readOnly: true,
},
/*
* True if `kArcAccountRestrictions` feature is enabled.
*/
isArcAccountRestrictionsEnabled_: {
type: Boolean,
value() {
return loadTimeData.getBoolean('isArcAccountRestrictionsEnabled');
},
readOnly: true,
},
/*
* True if the dialog is open for reauthentication.
*/
isReauthentication_: {
type: Boolean,
value: false,
},
/*
* True if the account should be available in ARC++ after addition.
*/
isAvailableInArc_: {
type: Boolean,
value: false,
},
/**
* User's email used in the sign-in flow.
*/
email_: {type: String, value: ''},
/**
* Hosted domain of the user's email used in the sign-in flow.
*/
hostedDomain_: {type: String, value: ''},
/**
* Whether secondary account sign-ins are allowed.
*/
isSecondaryGoogleAccountSigninAllowed_: {
type: Boolean,
value() {
return loadTimeData.getBoolean('secondaryGoogleAccountSigninAllowed');
},
},
/**
* Id of the screen that is currently displayed.
*/
currentView_: {
type: String,
value: '',
},
};
}
private loading_: boolean;
private verifyingAccount_: boolean;
private authenticator_: Authenticator|null;
private shouldSkipWelcomePage_: boolean;
private isArcAccountRestrictionsEnabled_: boolean;
private isReauthentication_: boolean;
private isAvailableInArc_: boolean;
private email_: string;
private hostedDomain_: string;
private isSecondaryGoogleAccountSigninAllowed_: boolean;
private currentView_: View;
/** Whether the login UI is loaded for signing in primary account. */
private isLoginPrimaryAccount_: boolean = false;
private browserProxy_: InlineLoginBrowserProxy =
InlineLoginBrowserProxyImpl.getInstance();
override ready() {
super.ready();
if (!this.isSecondaryGoogleAccountSigninAllowed_) {
// This can happen only if the user opened chrome://chrome-signin manually
// in the browser. Normally (in the account addition dialog) this will be
// handled earlier and a special error screen will be shown.
console.warn(
'SecondaryGoogleAccountSigninAllowed is set to false - aborting.');
return;
}
this.authenticator_ = new Authenticator(this.$.signinFrame);
this.addAuthenticatorListeners_();
this.browserProxy_.initialize();
}
override connectedCallback() {
super.connectedCallback();
this.addWebUiListener(
'load-authenticator',
(data: AuthParams) => this.loadAuthenticator_(data));
this.addWebUiListener('close-dialog', () => this.closeDialog_());
this.addWebUiListener(
'show-signin-error-page',
(data: SigninErrorPageData) => this.signinErrorShowView_(data));
}
private addAuthenticatorListeners_() {
assert(this.authenticator_);
this.authenticator_.addEventListener(
'dropLink', e => this.onDropLink_(e as CustomEvent<string>));
this.authenticator_.addEventListener(
'newWindow',
e => this.onNewWindow_(e as CustomEvent<NewWindowProperties>));
this.authenticator_.addEventListener('ready', () => this.onAuthReady_());
this.authenticator_.addEventListener(
'resize', e => this.onResize_(e as CustomEvent<string>));
this.authenticator_.addEventListener(
'authCompleted',
e => this.onAuthCompleted_(e as CustomEvent<AuthCompletedCredentials>));
this.authenticator_.addEventListener(
'showIncognito', () => this.onShowIncognito_());
this.authenticator_.addEventListener(
'getAccounts', () => this.onGetAccounts_());
this.authenticator_.addEventListener(
'getDeviceId', () => this.onGetDeviceId_());
}
private onDropLink_(e: CustomEvent<string>) {
// Navigate to the dropped link.
window.location.href = e.detail;
}
private onNewWindow_(e: CustomEvent<NewWindowProperties>) {
window.open(e.detail.targetUrl, '_blank');
e.detail.window.discard();
// On Chrome OS this dialog is always-on-top, so we have to close it if
// user opens a link in a new window.
this.closeDialog_();
}
private onAuthReady_() {
this.loading_ = false;
if (this.isLoginPrimaryAccount_) {
this.browserProxy_.recordAction('Signin_SigninPage_Shown');
}
this.browserProxy_.authenticatorReady();
}
private onResize_(e: CustomEvent<string>) {
this.browserProxy_.switchToFullTab(e.detail);
}
private onAuthCompleted_(e: CustomEvent<AuthCompletedCredentials>) {
this.verifyingAccount_ = true;
const credentials = e.detail;
if (this.isArcAccountRestrictionsEnabled_ && !this.isReauthentication_) {
credentials.isAvailableInArc = this.isAvailableInArc_;
}
this.browserProxy_.completeLogin(credentials);
}
private onShowIncognito_() {
this.browserProxy_.showIncognito();
}
private onGetAccounts_() {
this.browserProxy_.getAccounts().then(result => {
assert(this.authenticator_);
this.authenticator_.getAccountsResponse(result);
});
}
private onGetDeviceId_() {
this.browserProxy_.getDeviceId().then(deviceId => {
assert(this.authenticator_);
this.authenticator_.getDeviceIdResponse(deviceId);
});
}
/**
* Loads auth extension.
* @param data Parameters for auth extension.
*/
private loadAuthenticator_(data: AuthParams) {
assert(this.authenticator_);
this.authenticator_.load(data.authMode, data);
this.loading_ = true;
this.isLoginPrimaryAccount_ = data.isLoginPrimaryAccount;
// Skip welcome page for reauthentication.
if (data.email) {
this.isReauthentication_ = true;
}
this.switchToDefaultView_();
}
/**
* @param loading Indicates whether the page is loading.
* @param verifyingAccount Indicates whether the user account is being
* verified.
*/
private isSpinnerActive_(loading: boolean, verifyingAccount: boolean):
boolean {
return loading || verifyingAccount;
}
/**
* Closes the login dialog.
*/
private closeDialog_() {
this.browserProxy_.dialogClose();
}
/**
* Navigates to the welcome screen.
*/
private goToWelcomeScreen_() {
this.switchView_(View.WELCOME);
}
/**
* Navigates back in the web view if possible. Otherwise closes the dialog.
*/
private handleGoBack_() {
if (this.$.signinFrame.canGoBack()) {
this.$.signinFrame.back();
this.$.signinFrame.focus();
} else if (this.isWelcomePageEnabled_()) {
// Allow user go back to the welcome page, if it's enabled.
this.switchView_(View.WELCOME);
} else {
this.closeDialog_();
}
}
private getBackButtonIcon_(): string {
return isRTL() ? 'cr:chevron-right' : 'cr:chevron-left';
}
private getNextButtonLabel_(
currentView: View, isArcAccountRestrictionsEnabled: boolean): string {
if (currentView === View.SIGNIN_BLOCKED_BY_POLICY ||
currentView === View.SIGNIN_ERROR) {
return this.i18n('ok');
}
if (!isArcAccountRestrictionsEnabled) {
return this.i18n('ok');
}
return this.i18n('nextButtonLabel');
}
/**
* @param currentView Identifier of the view that is being shown.
* @param verifyingAccount Indicates whether the user account is being
* verified.
*/
private shouldShowBackButton_(currentView: View, verifyingAccount: boolean):
boolean {
return currentView === View.ADD_ACCOUNT && !verifyingAccount;
}
private shouldShowOkButton_(): boolean {
return this.currentView_ === View.WELCOME ||
this.currentView_ === View.SIGNIN_BLOCKED_BY_POLICY ||
this.currentView_ === View.SIGNIN_ERROR;
}
private shouldShowGaiaButtons_(): boolean {
return this.currentView_ === View.ADD_ACCOUNT;
}
/**
* Navigates to the default view.
*/
private switchToDefaultView_() {
const view = this.getDefaultView_();
if (this.isArcAccountRestrictionsEnabled_ &&
view === View.ARC_ACCOUNT_PICKER) {
const arcAccountPickerApp =
this.shadowRoot!.querySelector('arc-account-picker-app');
assert(arcAccountPickerApp);
arcAccountPickerApp.loadAccounts().then(
(accountsFound: boolean) => {
this.switchView_(
accountsFound ? View.ARC_ACCOUNT_PICKER : View.WELCOME);
},
(_error: Error) => {
this.switchView_(View.WELCOME);
});
return;
}
this.switchView_(view);
}
private getDefaultView_(): View {
if (this.isReauthentication_) {
return View.ADD_ACCOUNT;
}
if (this.isArcAccountRestrictionsEnabled_) {
const options = getAccountAdditionOptionsFromJSON(
InlineLoginBrowserProxyImpl.getInstance().getDialogArguments());
if (!!options && options.showArcAvailabilityPicker) {
return View.ARC_ACCOUNT_PICKER;
}
}
return this.shouldSkipWelcomePage_ ? View.ADD_ACCOUNT : View.WELCOME;
}
/**
* @param id identifier of the view that should be shown.
* @param enterAnimation enter animation for the new view.
* @param exitAnimation exit animation for the previous view.
*/
private switchView_(
id: View, enterAnimation: string = 'fade-in',
exitAnimation: string = 'fade-out') {
this.currentView_ = id;
this.$.viewManager.switchView(id, enterAnimation, exitAnimation);
this.dispatchEvent(new CustomEvent('switch-view-notify-for-testing'));
}
private isWelcomePageEnabled_(): boolean {
return !this.shouldSkipWelcomePage_ && !this.isReauthentication_;
}
/**
* Shows the sign-in blocked by policy screen if the user account is not
* allowed to sign-in. Or shows the sign-in error screen if any error occurred
* during the sign-in flow.
*/
private signinErrorShowView_(data: SigninErrorPageData) {
this.verifyingAccount_ = false;
if (data.signinBlockedByPolicy) {
this.set('email_', data.email);
this.set('hostedDomain_', data.hostedDomain);
this.set('deviceType_', data.deviceType);
this.switchView_(
View.SIGNIN_BLOCKED_BY_POLICY, 'no-animation', 'no-animation');
} else {
this.switchView_(View.SIGNIN_ERROR, 'no-animation', 'no-animation');
}
this.setFocusToWebview_();
}
private onOkButtonClick_() {
switch (this.currentView_) {
case View.WELCOME:
this.switchView_(View.ADD_ACCOUNT);
const welcomePageApp =
this.shadowRoot!.querySelector('welcome-page-app');
assert(welcomePageApp);
const skipChecked = welcomePageApp.isSkipCheckboxChecked();
this.browserProxy_.skipWelcomePage(skipChecked);
this.setFocusToWebview_();
break;
case View.SIGNIN_BLOCKED_BY_POLICY:
case View.SIGNIN_ERROR:
this.closeDialog_();
break;
}
}
private setFocusToWebview_() {
this.$.signinFrame.focus();
}
setAuthenticatorForTest(authenticator: Authenticator) {
this.authenticator_ = authenticator;
this.addAuthenticatorListeners_();
}
}
declare global {
interface HTMLElementTagNameMap {
'inline-login-app': InlineLoginAppElement;
}
}
customElements.define(InlineLoginAppElement.is, InlineLoginAppElement);