blob: c9793234264310a0a4c0fc233acd2b98770e3303 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview
* 'settings-sync-account-section' is the settings page containing sign-in
* settings.
*/
import {Polymer, html} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import '//resources/cr_elements/cr_action_menu/cr_action_menu.m.js';
import '//resources/cr_elements/cr_button/cr_button.m.js';
import '//resources/cr_elements/cr_icon_button/cr_icon_button.m.js';
import '//resources/cr_elements/icons.m.js';
import '//resources/cr_elements/shared_style_css.m.js';
import '//resources/cr_elements/shared_vars_css.m.js';
import {assert} from '//resources/js/assert.m.js';
import {WebUIListenerBehavior} from '//resources/js/web_ui_listener_behavior.m.js';
import '//resources/polymer/v3_0/iron-icon/iron-icon.js';
import './profile_info_browser_proxy.js';
import {SyncBrowserProxy, SyncBrowserProxyImpl, SyncStatus, StatusAction, StoredAccount} from './sync_browser_proxy.js';
import {loadTimeData} from '../i18n_setup.js';
import '../icons.js';
import '../prefs/prefs.js';
import {PrefsBehavior} from '../prefs/prefs_behavior.js';
import {Route, Router} from '../router.js';
import '../settings_shared_css.js';
/** @const {number} */
export const MAX_SIGNIN_PROMO_IMPRESSION = 10;
Polymer({
is: 'settings-sync-account-control',
_template: html`{__html_template__}`,
behaviors: [
WebUIListenerBehavior,
PrefsBehavior,
],
properties: {
/**
* Preferences state.
*/
prefs: {
type: Object,
notify: true,
},
/**
* The current sync status, supplied by parent element.
* @type {!SyncStatus}
*/
syncStatus: Object,
// String to be used as a title when the promo has an account.
promoLabelWithAccount: String,
// String to be used as title of the promo has no account.
promoLabelWithNoAccount: String,
// String to be used as a subtitle when the promo has an account.
promoSecondaryLabelWithAccount: String,
// String to be used as subtitle of the promo has no account.
promoSecondaryLabelWithNoAccount: String,
/**
* Proxy variable for syncStatus.signedIn to shield observer from being
* triggered multiple times whenever syncStatus changes.
* @private {boolean}
*/
signedIn_: {
type: Boolean,
computed: 'computeSignedIn_(syncStatus.signedIn)',
observer: 'onSignedInChanged_',
},
/** @private {!Array<!StoredAccount>} */
storedAccounts_: Object,
/** @private {?StoredAccount} */
shownAccount_: Object,
showingPromo: {
type: Boolean,
value: false,
reflectToAttribute: true,
},
// This property should be set by the parent only and should not change
// after the element is created.
embeddedInSubpage: {
type: Boolean,
reflectToAttribute: true,
},
// This property should be set by the parent only and should not change
// after the element is created.
hideButtons: {
type: Boolean,
value: false,
reflectToAttribute: true,
},
/** @private {boolean} */
shouldShowAvatarRow_: {
type: Boolean,
value: false,
computed: 'computeShouldShowAvatarRow_(storedAccounts_, syncStatus,' +
'storedAccounts_.length, syncStatus.signedIn)',
observer: 'onShouldShowAvatarRowChange_',
},
/** @private */
subLabel_: {
type: String,
computed: 'computeSubLabel_(promoSecondaryLabelWithAccount,' +
'promoSecondaryLabelWithNoAccount, shownAccount_)',
},
/** @private */
showSetupButtons_: {
type: Boolean,
computed: 'computeShowSetupButtons_(' +
'hideButtons, syncStatus.firstSetupInProgress)',
},
},
observers: [
'onShownAccountShouldChange_(storedAccounts_, syncStatus)',
],
/** @private {?SyncBrowserProxy} */
syncBrowserProxy_: null,
created() {
this.syncBrowserProxy_ = SyncBrowserProxyImpl.getInstance();
},
/** @override */
attached() {
this.syncBrowserProxy_.getStoredAccounts().then(
this.handleStoredAccounts_.bind(this));
this.addWebUIListener(
'stored-accounts-updated', this.handleStoredAccounts_.bind(this));
},
/**
* Records the following user actions:
* - Signin_Impression_FromSettings and
* - Signin_ImpressionWithAccount_FromSettings
* - Signin_ImpressionWithNoAccount_FromSettings
* @private
*/
recordImpressionUserActions_() {
assert(!this.syncStatus.signedIn);
assert(this.shownAccount_ !== undefined);
chrome.metricsPrivate.recordUserAction('Signin_Impression_FromSettings');
if (this.shownAccount_) {
chrome.metricsPrivate.recordUserAction(
'Signin_ImpressionWithAccount_FromSettings');
} else {
chrome.metricsPrivate.recordUserAction(
'Signin_ImpressionWithNoAccount_FromSettings');
}
},
/**
* @return {boolean}
* @private
*/
computeSignedIn_() {
return !!this.syncStatus && !!this.syncStatus.signedIn;
},
/** @private */
onSignedInChanged_() {
if (this.embeddedInSubpage) {
this.showingPromo = true;
return;
}
if (!this.showingPromo && !this.syncStatus.signedIn &&
this.syncBrowserProxy_.getPromoImpressionCount() <
MAX_SIGNIN_PROMO_IMPRESSION) {
this.showingPromo = true;
this.syncBrowserProxy_.incrementPromoImpressionCount();
} else {
// Turn off the promo if the user is signed in.
this.showingPromo = false;
}
if (!this.syncStatus.signedIn && this.shownAccount_ !== undefined) {
this.recordImpressionUserActions_();
}
},
/**
* @param {string} labelWithAccount
* @param {string} labelWithNoAccount
* @return {string}
* @private
*/
getLabel_(labelWithAccount, labelWithNoAccount) {
return this.shownAccount_ ? labelWithAccount : labelWithNoAccount;
},
/**
* @return {string}
* @private
*/
computeSubLabel_() {
return this.getLabel_(
this.promoSecondaryLabelWithAccount,
this.promoSecondaryLabelWithNoAccount);
},
/**
* @param {string} label
* @param {string} name
* @return {string}
* @private
*/
getSubstituteLabel_(label, name) {
return loadTimeData.substituteString(label, name);
},
/**
* @param {string} label
* @param {string} account
* @return {string}
* @private
*/
getAccountLabel_(label, account) {
if (this.syncStatus.firstSetupInProgress) {
return this.syncStatus.statusText || account;
}
return this.syncStatus.signedIn && !this.syncStatus.hasError &&
!this.syncStatus.disabled ?
loadTimeData.substituteString(label, account) :
account;
},
/**
* @param {?string} image
* @return {string}
* @private
*/
getAccountImageSrc_(image) {
// image can be undefined if the account has not set an avatar photo.
return image || 'chrome://theme/IDR_PROFILE_AVATAR_PLACEHOLDER_LARGE';
},
/**
* Returns the class of the sync icon.
* @return {string}
* @private
*/
getSyncIconStyle_() {
if (this.syncStatus.disabled) {
return 'sync-disabled';
}
if (!this.syncStatus.hasError) {
return 'sync';
}
// Specific error cases below.
if (this.syncStatus.hasUnrecoverableError) {
return 'sync-problem';
}
if (this.syncStatus.statusAction ===
StatusAction.REAUTHENTICATE) {
return 'sync-paused';
}
return 'sync-problem';
},
/**
* Returned value must match one of iron-icon's settings:(*) icon name.
* @return {string}
* @private
*/
getSyncIcon_() {
switch (this.getSyncIconStyle_()) {
case 'sync-problem':
return 'settings:sync-problem';
case 'sync-paused':
return 'settings:sync-disabled';
default:
return 'cr:sync';
}
},
/**
* @param {string} accountName
* @param {string} syncErrorLabel
* @param {string} syncPasswordsOnlyErrorLabel
* @param {string} authErrorLabel
* @param {string} disabledLabel
* @return {string}
* @private
*/
getAvatarRowTitle_(
accountName, syncErrorLabel, syncPasswordsOnlyErrorLabel,
authErrorLabel, disabledLabel) {
if (this.syncStatus.disabled) {
return disabledLabel;
}
if (!this.syncStatus.hasError) {
return accountName;
}
// Specific error cases below.
if (this.syncStatus.hasUnrecoverableError) {
return syncErrorLabel;
}
if (this.syncStatus.statusAction ===
StatusAction.REAUTHENTICATE) {
return authErrorLabel;
}
if (this.syncStatus.hasPasswordsOnlyError) {
return syncPasswordsOnlyErrorLabel;
}
return syncErrorLabel;
},
/**
* Determines if the sync button should be disabled in response to
* either a first setup flow or chrome sign-in being disabled.
* @return {boolean}
* @private
*/
shouldDisableSyncButton_() {
if (this.hideButtons || this.prefs === undefined) {
return this.computeShowSetupButtons_();
}
return !!this.syncStatus.firstSetupInProgress ||
!this.getPref('signin.allowed_on_next_startup').value;
},
/**
* @return {boolean}
* @private
*/
shouldShowTurnOffButton_() {
// <if expr="chromeos">
if (this.syncStatus.domain) {
// Chrome OS cannot delete the user's profile like other platforms, so
// hide the turn off sync button for enterprise users who are not
// allowed to sign out.
return false;
}
// </if>
return !this.hideButtons && !this.showSetupButtons_ &&
!!this.syncStatus.signedIn;
},
/**
* @return {boolean}
* @private
*/
shouldShowErrorActionButton_() {
if (this.embeddedInSubpage &&
this.syncStatus.statusAction ===
StatusAction.ENTER_PASSPHRASE) {
// In a subpage the passphrase button is not required.
return false;
}
return !this.hideButtons && !this.showSetupButtons_ &&
!!this.syncStatus.signedIn && !!this.syncStatus.hasError &&
this.syncStatus.statusAction !== StatusAction.NO_ACTION;
},
/**
* @param {!Array<!StoredAccount>} accounts
* @private
*/
handleStoredAccounts_(accounts) {
this.storedAccounts_ = accounts;
},
/**
* @return {boolean}
* @private
*/
computeShouldShowAvatarRow_() {
if (this.storedAccounts_ === undefined || this.syncStatus === undefined) {
return false;
}
return this.syncStatus.signedIn || this.storedAccounts_.length > 0;
},
/** @private */
onErrorButtonTap_() {
const router = Router.getInstance();
const routes =
/** @type {{ SIGN_OUT: !Route }} */ (router.getRoutes());
switch (this.syncStatus.statusAction) {
case StatusAction.REAUTHENTICATE:
this.syncBrowserProxy_.startSignIn();
break;
case StatusAction.SIGNOUT_AND_SIGNIN:
if (this.syncStatus.domain) {
router.navigateTo(routes.SIGN_OUT);
} else {
// Silently sign the user out without deleting their profile and
// prompt them to sign back in.
this.syncBrowserProxy_.signOut(false);
this.syncBrowserProxy_.startSignIn();
}
break;
case StatusAction.UPGRADE_CLIENT:
router.navigateTo(router.getRoutes().ABOUT);
break;
case StatusAction.RETRIEVE_TRUSTED_VAULT_KEYS:
this.syncBrowserProxy_.startKeyRetrieval();
break;
case StatusAction.ENTER_PASSPHRASE:
case StatusAction.CONFIRM_SYNC_SETTINGS:
default:
router.navigateTo(
/** @type {{ SYNC: !Route }} */ (router.getRoutes())
.SYNC);
}
},
/** @private */
onSigninTap_() {
// <if expr="not chromeos">
this.syncBrowserProxy_.startSignIn();
// </if>
// <if expr="chromeos">
// Chrome OS is always signed-in, so just turn on sync.
this.syncBrowserProxy_.turnOnSync();
// </if>
// Need to close here since one menu item also triggers this function.
if (this.$$('#menu')) {
/** @type {!CrActionMenuElement} */ (this.$$('#menu')).close();
}
},
/** @private */
onSignoutTap_() {
this.syncBrowserProxy_.signOut(false /* deleteProfile */);
/** @type {!CrActionMenuElement} */ (this.$$('#menu')).close();
},
/** @private */
onSyncButtonTap_() {
assert(this.shownAccount_);
assert(this.storedAccounts_.length > 0);
const isDefaultPromoAccount =
(this.shownAccount_.email === this.storedAccounts_[0].email);
this.syncBrowserProxy_.startSyncingWithEmail(
this.shownAccount_.email, isDefaultPromoAccount);
},
/** @private */
onTurnOffButtonTap_() {
/* This will route to people_page's disconnect dialog. */
const router = Router.getInstance();
router.navigateTo(
/** @type {{ SIGN_OUT: !Route }} */ (router.getRoutes())
.SIGN_OUT);
},
/** @private */
onMenuButtonTap_() {
const actionMenu =
/** @type {!CrActionMenuElement} */ (this.$$('#menu'));
actionMenu.showAt(assert(this.$$('#dropdown-arrow')));
},
/** @private */
onShouldShowAvatarRowChange_() {
// Close dropdown when avatar-row hides, so if it appears again, the menu
// won't be open by default.
const actionMenu = this.$$('#menu');
if (!this.shouldShowAvatarRow_ && actionMenu && actionMenu.open) {
actionMenu.close();
}
},
/**
* @param {!{model:
* !{item: !StoredAccount},
* }} e
* @private
*/
onAccountTap_(e) {
this.shownAccount_ = e.model.item;
/** @type {!CrActionMenuElement} */ (this.$$('#menu')).close();
},
/** @private */
onShownAccountShouldChange_() {
if (this.storedAccounts_ === undefined || this.syncStatus === undefined) {
return;
}
if (this.syncStatus.signedIn) {
for (let i = 0; i < this.storedAccounts_.length; i++) {
if (this.storedAccounts_[i].email ===
this.syncStatus.signedInUsername) {
this.shownAccount_ = this.storedAccounts_[i];
return;
}
}
} else {
const firstStoredAccount =
(this.storedAccounts_.length > 0) ? this.storedAccounts_[0] : null;
// Sign-in impressions should be recorded in the following cases:
// 1. When the promo is first shown, i.e. when |shownAccount_| is
// initialized;
// 2. When the impression account state changes, i.e. promo impression
// state changes (WithAccount -> WithNoAccount) or
// (WithNoAccount -> WithAccount).
const shouldRecordImpression = (this.shownAccount_ === undefined) ||
(!this.shownAccount_ && firstStoredAccount) ||
(this.shownAccount_ && !firstStoredAccount);
this.shownAccount_ = firstStoredAccount;
if (shouldRecordImpression) {
this.recordImpressionUserActions_();
}
}
},
/**
* @return {boolean}
* @private
*/
computeShowSetupButtons_() {
return !this.hideButtons && !!this.syncStatus.firstSetupInProgress;
},
/** @private */
onSetupCancel_() {
this.fire('sync-setup-done', false);
},
/** @private */
onSetupConfirm_() {
this.fire('sync-setup-done', true);
},
});