chromium/chrome/browser/resources/settings/safety_hub/safety_hub_page.ts

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

/**
 * @fileoverview
 * 'settings-safety-hub-page' is the settings page that presents the safety
 * state of Chrome.
 */

import 'chrome://resources/cr_elements/cr_shared_vars.css.js';
import './safety_hub_card.js';
import './safety_hub_module.js';

import {PrefsMixin} from '/shared/settings/prefs/prefs_mixin.js';
import {I18nMixin} from 'chrome://resources/cr_elements/i18n_mixin.js';
import {WebUiListenerMixin} from 'chrome://resources/cr_elements/web_ui_listener_mixin.js';
import {assertNotReached} from 'chrome://resources/js/assert.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import {PasswordManagerImpl, PasswordManagerPage} from '../autofill_page/password_manager_proxy.js';
import type {MetricsBrowserProxy, SafetyHubCardState} from '../metrics_browser_proxy.js';
import {MetricsBrowserProxyImpl, SafetyHubModuleType, SafetyHubSurfaces} from '../metrics_browser_proxy.js';
import {RelaunchMixin, RestartType} from '../relaunch_mixin.js';
import {routes} from '../route.js';
import {RouteObserverMixin, Router} from '../router.js';

import type {CardInfo, NotificationPermission, SafetyHubBrowserProxy, UnusedSitePermissions} from './safety_hub_browser_proxy.js';
import {CardState, SafetyHubBrowserProxyImpl, SafetyHubEvent} from './safety_hub_browser_proxy.js';
import type {SiteInfo} from './safety_hub_module.js';
import {getTemplate} from './safety_hub_page.html.js';

export interface SettingsSafetyHubPageElement {
  $: {
    passwords: HTMLElement,
    safeBrowsing: HTMLElement,
    version: HTMLElement,
  };
}

const SettingsSafetyHubPageElementBase = RouteObserverMixin(
    RelaunchMixin(PrefsMixin(WebUiListenerMixin(I18nMixin(PolymerElement)))));

export class SettingsSafetyHubPageElement extends
    SettingsSafetyHubPageElementBase {
  static get is() {
    return 'settings-safety-hub-page';
  }

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

  static get properties() {
    return {
      // The object that holds data of Password Check card.
      passwordCardData_: Object,

      // The object that holds data of Version Check card.
      versionCardData_: Object,

      // The object that holds data of Safe Browsing card.
      safeBrowsingCardData_: Object,

      // Whether Notification Permissions module should be visible.
      showNotificationPermissions_: {
        type: Boolean,
        value: false,
      },

      // Whether Unused Site Permissions module should be visible.
      showUnusedSitePermissions_: {
        type: Boolean,
        value: false,
      },

      // Whether Extensions module should be visible.
      showExtensions_: {
        type: Boolean,
        value: false,
      },

      showNoRecommendationsState_: {
        type: Boolean,
        computed:
            'computeShowNoRecommendationsState_(showUnusedSitePermissions_.*, showExtensions_.*, showNotificationPermissions_.*)',
      },

      userEducationItemList_: Array,

      // Whether the data for notification permissions is ready.
      hasDataForNotificationPermissions_: Boolean,

      // Whether the data for unused site permissions is ready.
      hasDataForUnusedPermissions_: Boolean,

      // Whether the data for extensions is ready.
      hasDataForExtensions_: Boolean,

      // String that identifies version card's role announced by accessibility
      // voiceover.
      versionCardRole_: {
        type: String,
        computed: 'computeVersionCardRole_(versionCardData_)',
      },

      // String that identifies version card's description announced by
      // accessibility voiceover.
      versionCardAriaDescription_: {
        type: String,
        computed: 'computeVersionCardAriaDescription_(versionCardData_)',
      },

    };
  }

  static get observers() {
    return [
      'onAllModulesLoaded_(passwordCardData_, versionCardData_, safeBrowsingCardData_, hasDataForUnusedPermissions_, hasDataForNotificationPermissions_, hasDataForExtensions_)',
      'onSafeBrowsingPrefChanged_(prefs.generated.safe_browsing)',
    ];
  }

  private passwordCardData_: CardInfo;
  private versionCardData_: CardInfo;
  private safeBrowsingCardData_: CardInfo;
  private showNotificationPermissions_: boolean;
  private hasDataForNotificationPermissions_: boolean;
  private showUnusedSitePermissions_: boolean;
  private hasDataForUnusedPermissions_: boolean;
  private showNoRecommendationsState_: boolean;
  private showExtensions_: boolean;
  private hasDataForExtensions_: boolean;
  private shouldRecordMetric_: boolean = false;
  private userEducationItemList_: SiteInfo[];
  private versionCardRole_: string;
  private versionCardAriaDescription_: string;
  private browserProxy_: SafetyHubBrowserProxy =
      SafetyHubBrowserProxyImpl.getInstance();
  private metricsBrowserProxy_: MetricsBrowserProxy =
      MetricsBrowserProxyImpl.getInstance();

  override connectedCallback() {
    this.initializeCards_();
    this.initializeModules_();
    this.initializeUserEducation_();

    super.connectedCallback();
  }

  override currentRouteChanged() {
    if (Router.getInstance().getCurrentRoute() !== routes.SAFETY_HUB) {
      return;
    }
    // When the user navigates to the Safety Hub page, any active menu
    // notification is dismissed.
    this.browserProxy_.dismissActiveMenuNotification();

    // Record a visit to Safety Hub page if the user is still on the SH page
    // after 20 seconds.
    setTimeout(() => {
      if (Router.getInstance().getCurrentRoute() === routes.SAFETY_HUB) {
        this.browserProxy_.recordSafetyHubPageVisit();
      }
    }, 20000);

    this.metricsBrowserProxy_.recordSafetyHubImpression(
        SafetyHubSurfaces.SAFETY_HUB_PAGE);
    this.metricsBrowserProxy_.recordSafetyHubInteraction(
        SafetyHubSurfaces.SAFETY_HUB_PAGE);

    // Only record the metrics when the user navigates to the Safety Hub page.
    this.shouldRecordMetric_ = true;
    this.onAllModulesLoaded_();
  }

  private initializeCards_() {
    // TODO(crbug.com/40267370): Add listeners for Password and Version cards.
    this.browserProxy_.getPasswordCardData().then((data: CardInfo) => {
      this.passwordCardData_ = data;
    });

    this.browserProxy_.getSafeBrowsingCardData().then((data: CardInfo) => {
      this.safeBrowsingCardData_ = data;
    });

    this.browserProxy_.getVersionCardData().then((data: CardInfo) => {
      this.versionCardData_ = data;
    });
  }

  private initializeModules_() {
    this.addWebUiListener(
        SafetyHubEvent.NOTIFICATION_PERMISSIONS_MAYBE_CHANGED,
        (sites: NotificationPermission[]) =>
            this.onNotificationPermissionListChanged_(sites));

    this.addWebUiListener(
        SafetyHubEvent.UNUSED_PERMISSIONS_MAYBE_CHANGED,
        (sites: UnusedSitePermissions[]) =>
            this.onUnusedSitePermissionListChanged_(sites));

    this.addWebUiListener(
        SafetyHubEvent.EXTENSIONS_CHANGED,
        (num: number) => this.onExtensionsChanged_(num));

    this.browserProxy_.getNotificationPermissionReview().then(
        (sites: NotificationPermission[]) =>
            this.onNotificationPermissionListChanged_(sites));

    this.browserProxy_.getRevokedUnusedSitePermissionsList().then(
        (sites: UnusedSitePermissions[]) =>
            this.onUnusedSitePermissionListChanged_(sites));

    this.browserProxy_.getNumberOfExtensionsThatNeedReview().then(
        (num: number) => this.onExtensionsChanged_(num));
  }

  private initializeUserEducation_() {
    this.userEducationItemList_ = [
      {
        origin: this.i18n('safetyHubUserEduDataHeader'),
        detail: this.i18nAdvanced('safetyHubUserEduDataSubheader'),
        icon: 'settings20:chrome-filled',
      },
      {
        origin: this.i18n('safetyHubUserEduIncognitoHeader'),
        detail: this.i18nAdvanced('safetyHubUserEduIncognitoSubheader'),
        icon: 'settings20:incognito-unfilled',
      },
      {
        origin: this.i18n('safetyHubUserEduSafeBrowsingHeader'),
        detail: this.i18nAdvanced('safetyHubUserEduSafeBrowsingSubheader'),
        icon: 'cr:security',
      },
    ];
  }

  private onPasswordsClick_() {
    this.metricsBrowserProxy_.recordSafetyHubCardStateClicked(
        'Settings.SafetyHub.PasswordsCard.StatusOnClick',
        this.passwordCardData_.state as unknown as SafetyHubCardState);
    this.browserProxy_.recordSafetyHubInteraction();

    PasswordManagerImpl.getInstance().showPasswordManager(
        PasswordManagerPage.CHECKUP);
  }

  private onPasswordsKeyPress_(e: KeyboardEvent) {
    e.stopPropagation();
    if (this.isEnterOrSpaceClicked_(e)) {
      this.onPasswordsClick_();
    }
  }

  private onVersionClick_() {
    this.metricsBrowserProxy_.recordSafetyHubCardStateClicked(
        'Settings.SafetyHub.VersionCard.StatusOnClick',
        this.versionCardData_.state as unknown as SafetyHubCardState);
    this.browserProxy_.recordSafetyHubInteraction();

    if (this.versionCardData_.state === CardState.WARNING) {
      // Optional parameter alwaysShowDialog is set to true to always show the
      // confirmation dialog regardless of the incognito windows open.
      this.performRestart(RestartType.RELAUNCH, true);
    } else {
      Router.getInstance().navigateTo(
          routes.ABOUT, /* dynamicParams= */ undefined,
          /* removeSearch= */ true);
    }
  }

  private onEducationLinkClick_(event: CustomEvent<HTMLAnchorElement>) {
    this.browserProxy_.recordSafetyHubInteraction();
    const headerString =
        event.detail.querySelector('.site-representation')!.textContent;

    switch (headerString) {
      case this.i18n('safetyHubUserEduDataHeader'):
        this.metricsBrowserProxy_.recordAction(
            'Settings.SafetyHub.SafetyToolsLinkClicked');
        break;
      case this.i18n('safetyHubUserEduIncognitoHeader'):
        this.metricsBrowserProxy_.recordAction(
            'Settings.SafetyHub.IncognitoLinkClicked');
        break;
      case this.i18n('safetyHubUserEduSafeBrowsingHeader'):
        this.metricsBrowserProxy_.recordAction(
            'Settings.SafetyHub.SafeBrowsingLinkClicked');
        break;
      default:
        assertNotReached();
    }
  }

  private onVersionKeyPress_(e: KeyboardEvent) {
    e.stopPropagation();
    if (this.isEnterOrSpaceClicked_(e)) {
      this.onVersionClick_();
    }
  }

  private onSafeBrowsingPrefChanged_() {
    this.browserProxy_.getSafeBrowsingCardData().then((data: CardInfo) => {
      this.safeBrowsingCardData_ = data;
    });
  }

  private onSafeBrowsingClick_() {
    this.metricsBrowserProxy_.recordSafetyHubCardStateClicked(
        'Settings.SafetyHub.SafeBrowsingCard.StatusOnClick',
        this.safeBrowsingCardData_.state as unknown as SafetyHubCardState);
    this.browserProxy_.recordSafetyHubInteraction();

    Router.getInstance().navigateTo(
        routes.SECURITY, /* dynamicParams= */ undefined,
        /* removeSearch= */ true);
  }

  private onSafeBrowsingKeyPress_(e: KeyboardEvent) {
    e.stopPropagation();
    if (this.isEnterOrSpaceClicked_(e)) {
      this.onSafeBrowsingClick_();
    }
  }

  private onNotificationPermissionListChanged_(permissions:
                                                   NotificationPermission[]) {
    // The module should be visible if there is any item on the list, or if
    // there is no item on the list but the list was shown before.
    this.showNotificationPermissions_ =
        permissions.length > 0 || this.showNotificationPermissions_;
    this.hasDataForNotificationPermissions_ = true;
  }

  private onUnusedSitePermissionListChanged_(permissions:
                                                 UnusedSitePermissions[]) {
    // The module should be visible if there is any item on the list, or if
    // there is no item on the list but the list was shown before.
    this.showUnusedSitePermissions_ =
        permissions.length > 0 || this.showUnusedSitePermissions_;
    this.hasDataForUnusedPermissions_ = true;
  }

  private computeShowNoRecommendationsState_(): boolean {
    return !(
        this.showUnusedSitePermissions_ || this.showNotificationPermissions_ ||
        this.showExtensions_);
  }

  private onExtensionsChanged_(numberOfExtensions: number) {
    this.showExtensions_ = !!numberOfExtensions;
    this.hasDataForExtensions_ = true;
  }

  private computeVersionCardRole_(): string {
    return this.versionCardData_.state === CardState.WARNING ? 'button' : 'link';
  }

  private computeVersionCardAriaDescription_(): string {
    return this.versionCardData_.state === CardState.WARNING ?
        this.i18n('safetyHubVersionRelaunchAriaLabel') :
        this.i18n('safetyHubVersionNavigationAriaLabel');
  }

  private isEnterOrSpaceClicked_(e: KeyboardEvent): boolean {
    return e.key === 'Enter' || e.key === ' ';
  }

  private onAllModulesLoaded_() {
    // If the metrics are recorded already, don't record again.
    if (!this.shouldRecordMetric_) {
      return;
    }

    // Wait till the data of the cards be ready.
    if (!this.passwordCardData_ || !this.safeBrowsingCardData_ ||
        !this.versionCardData_) {
      return;
    }

    // Wait till the data of the modules be ready.
    if (!this.hasDataForUnusedPermissions_ ||
        !this.hasDataForNotificationPermissions_ ||
        !this.hasDataForExtensions_) {
      return;
    }

    this.shouldRecordMetric_ = false;
    let hasAnyWarning: boolean = false;
    // TODO(crbug.com/40267370): Iterate over the cards/modules with for loop.
    if (this.passwordCardData_.state !== CardState.SAFE) {
      this.metricsBrowserProxy_.recordSafetyHubModuleWarningImpression(
          SafetyHubModuleType.PASSWORDS);
      hasAnyWarning = true;
    }

    if (this.safeBrowsingCardData_.state !== CardState.SAFE) {
      this.metricsBrowserProxy_.recordSafetyHubModuleWarningImpression(
          SafetyHubModuleType.SAFE_BROWSING);
      hasAnyWarning = true;
    }

    if (this.versionCardData_.state !== CardState.SAFE) {
      this.metricsBrowserProxy_.recordSafetyHubModuleWarningImpression(
          SafetyHubModuleType.VERSION);
      hasAnyWarning = true;
    }

    if (this.showNotificationPermissions_) {
      this.metricsBrowserProxy_.recordSafetyHubModuleWarningImpression(
          SafetyHubModuleType.NOTIFICATIONS);
      hasAnyWarning = true;
    }

    if (this.showUnusedSitePermissions_) {
      this.metricsBrowserProxy_.recordSafetyHubModuleWarningImpression(
          SafetyHubModuleType.PERMISSIONS);
      hasAnyWarning = true;
    }

    if (this.showExtensions_) {
      this.metricsBrowserProxy_.recordSafetyHubModuleWarningImpression(
          SafetyHubModuleType.EXTENSIONS);
      hasAnyWarning = true;
    }

    this.metricsBrowserProxy_.recordSafetyHubDashboardAnyWarning(hasAnyWarning);
  }
}

declare global {
  interface HTMLElementTagNameMap {
    'settings-safety-hub-page': SettingsSafetyHubPageElement;
  }
}

customElements.define(
    SettingsSafetyHubPageElement.is, SettingsSafetyHubPageElement);