chromium/chrome/browser/resources/password_manager/sharing/share_password_confirmation_dialog.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.

import 'chrome://resources/cr_elements/cr_hidden_style.css.js';
import 'chrome://resources/cr_elements/cr_dialog/cr_dialog.js';
import 'chrome://resources/cr_elements/cr_button/cr_button.js';
import './metrics_utils.js';
import './share_password_dialog_header.js';
import './share_password_group_avatar.js';
import '../site_favicon.js';

import type {CrDialogElement} from 'chrome://resources/cr_elements/cr_dialog/cr_dialog.js';
import {I18nMixin} from 'chrome://resources/cr_elements/i18n_mixin.js';
import {assertNotReached} from 'chrome://resources/js/assert.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import type {PasswordManagerProxy} from '../password_manager_proxy.js';
import {PasswordManagerImpl} from '../password_manager_proxy.js';
import {UserUtilMixin} from '../user_utils_mixin.js';

import {PasswordSharingActions, recordPasswordSharingInteraction} from './metrics_utils.js';
import {getTemplate} from './share_password_confirmation_dialog.html.js';

export interface SharePasswordConfirmationDialogElement {
  $: {
    animation: HTMLElement,
    header: HTMLElement,
    cancel: HTMLElement,
    done: HTMLElement,
    dialog: CrDialogElement,
    senderAvatar: HTMLImageElement,
    recipientAvatar: HTMLElement,
    description: HTMLElement,
    footerDescription: HTMLElement,
  };
}

// Five seconds in milliseconds.
const FIVE_SECONDS = 5000;

enum ConfirmationDialogStage {
  LOADING,
  CANCELED,
  SUCCESS,
}

const SharePasswordConfirmationDialogElementBase =
    UserUtilMixin(I18nMixin(PolymerElement));

export class SharePasswordConfirmationDialogElement extends
    SharePasswordConfirmationDialogElementBase {
  static get is() {
    return 'share-password-confirmation-dialog';
  }

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

  static get properties() {
    return {
      dialogStage_: {
        type: Number,
        observer: 'stateChange_',
      },

      password: Object,
      passwordName: String,
      iconUrl: String,

      recipients: {
        type: Array,
        value: [],
      },

      dialogStageEnum_: {
        type: Object,
        value: ConfirmationDialogStage,
        readOnly: true,
      },
    };
  }

  password: chrome.passwordsPrivate.PasswordUiEntry;
  passwordName: string;
  iconUrl: string;
  recipients: chrome.passwordsPrivate.RecipientInfo[];
  private dialogStage_: ConfirmationDialogStage =
      ConfirmationDialogStage.LOADING;
  private passwordManager_: PasswordManagerProxy =
      PasswordManagerImpl.getInstance();

  override ready() {
    super.ready();

    // Start the animation after all elements have been loaded.
    setTimeout(() => {
      this.$.animation.classList.add('loading');
    }, 0);

    // The user has 5 seconds to cancel the share action while loading/sharing
    // animation is in progress.
    setTimeout(() => {
      if (this.isStage_(ConfirmationDialogStage.CANCELED)) {
        return;
      }
      this.passwordManager_.sharePassword(this.password.id, this.recipients);
      this.dialogStage_ = ConfirmationDialogStage.SUCCESS;
    }, FIVE_SECONDS);
  }

  private isStage_(stage: ConfirmationDialogStage): boolean {
    return this.dialogStage_ === stage;
  }

  private stateChange_() {
    // Don't reset focus on loading stage. Initially it is set correctly.
    if (this.isStage_(ConfirmationDialogStage.LOADING)) {
      return;
    }
    // Force the screen reader to focus on the updated dialog header.
    if (document.activeElement instanceof HTMLElement) {
      document.activeElement.blur();
    }

    this.$.dialog.focus();
  }


  private getDialogTitle_(): string {
    switch (this.dialogStage_) {
      case ConfirmationDialogStage.LOADING:
        return this.i18n('shareDialogLoadingTitle');
      case ConfirmationDialogStage.CANCELED:
        return this.i18n('shareDialogCanceledTitle');
      case ConfirmationDialogStage.SUCCESS:
        return this.i18n('shareDialogSuccessTitle');
      default:
        assertNotReached();
    }
  }

  private getSuccessDescription_(): TrustedHTML {
    if (this.recipients.length > 1) {
      return this.i18nAdvanced(
          'sharePasswordConfirmationDescriptionMultipleRecipients', {
            substitutions: [
              this.passwordName,
            ],
          });
    }
    return this.i18nAdvanced(
        'sharePasswordConfirmationDescriptionSingleRecipient', {
          substitutions: [
            this.recipients[0].displayName,
            this.passwordName,
          ],
        });
  }

  private hasSecureChangePasswordUrl_(): boolean {
    const url = this.password.changePasswordUrl;
    return !!url && (url.startsWith('https://'));
  }

  private getFooterDescription_(): TrustedHTML {
    // Only for Android Apps that don't have affiliated website, change password
    // url can't be generated.
    if (!this.password.changePasswordUrl) {
      return this.i18nAdvanced('sharePasswordConfirmationFooterAndroidApp');
    }

    // Don't insert change password url as '<a href>' for 'non-https' urls.
    return this.i18nAdvanced('sharePasswordConfirmationFooterWebsite', {
      substitutions: [
        this.hasSecureChangePasswordUrl_() ?
            `<a href='${this.password.changePasswordUrl}' target='_blank'>${
                this.passwordName}</a>` :
            this.passwordName,
      ],
    });
  }

  private onFooterClick_(e: Event) {
    const element = e.target as HTMLElement;
    if (element.tagName === 'A') {
      recordPasswordSharingInteraction(
          PasswordSharingActions.CONFIRMATION_DIALOG_CHANGE_PASSWORD_CLICKED);
    }
  }

  private onClickDone_() {
    this.dispatchEvent(
        new CustomEvent('close', {bubbles: true, composed: true}));
  }

  private onClickCancel_() {
    // Ignore the click if a race with the 5 second timeout occurred.
    if (this.isStage_(ConfirmationDialogStage.SUCCESS)) {
      return;
    }
    recordPasswordSharingInteraction(
        PasswordSharingActions.CONFIRMATION_DIALOG_SHARING_CANCELED);
    this.dialogStage_ = ConfirmationDialogStage.CANCELED;
    this.$.animation.classList.remove('loading');
  }
}

declare global {
  interface HTMLElementTagNameMap {
    'share-password-confirmation-dialog':
        SharePasswordConfirmationDialogElement;
  }
}

customElements.define(
    SharePasswordConfirmationDialogElement.is,
    SharePasswordConfirmationDialogElement);