chromium/chrome/browser/resources/ash/settings/internet_page/esim_rename_dialog.ts

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

/**
 * @fileoverview Polymer element to rename eSIM profile name
 */

import 'chrome://resources/ash/common/cellular_setup/cellular_setup_icons.html.js';
import 'chrome://resources/ash/common/cr_elements/cr_dialog/cr_dialog.js';
import 'chrome://resources/ash/common/cr_elements/cr_input/cr_input.js';
import 'chrome://resources/polymer/v3_0/iron-flex-layout/iron-flex-layout-classes.js';
import '../settings_shared.css.js';

import {getESimProfile} from 'chrome://resources/ash/common/cellular_setup/esim_manager_utils.js';
import {OncMojo} from 'chrome://resources/ash/common/network/onc_mojo.js';
import {CrDialogElement} from 'chrome://resources/ash/common/cr_elements/cr_dialog/cr_dialog.js';
import {CrInputElement} from 'chrome://resources/ash/common/cr_elements/cr_input/cr_input.js';
import {I18nMixin} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js';
import {stringToMojoString16} from 'chrome://resources/js/mojo_type_util.js';
import {ESimOperationResult, ESimProfileRemote} from 'chrome://resources/mojo/chromeos/ash/services/cellular_setup/public/mojom/esim_manager.mojom-webui.js';
import {NetworkType} from 'chrome://resources/mojo/chromeos/services/network_config/public/mojom/network_types.mojom-webui.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import {getTemplate} from './esim_rename_dialog.html.js';

const MAX_INPUT_LENGTH = 20;

const MIN_INPUT_LENGTH = 1;

const EMOJI_REGEX_EXP =
    /(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])/gi;

export interface EsimRenameDialogElement {
  $: {
    profileRenameDialog: CrDialogElement,
    warningMessage: HTMLElement,
  };
}

const EsimRenameDialogElementBase = I18nMixin(PolymerElement);

export class EsimRenameDialogElement extends EsimRenameDialogElementBase {
  static get is() {
    return 'esim-rename-dialog' as const;
  }

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

  static get properties() {
    return {
      /** Used to reference the MAX_INPUT_LENGTH constant in HTML. */
      maxInputLength: {
        type: Number,
        value: MAX_INPUT_LENGTH,
        readonly: true,
      },

      networkState: {
        type: Object,
        value: null,
      },

      showCellularDisconnectWarning: {
        type: Boolean,
        value: false,
      },

      errorMessage_: {
        type: String,
        value: '',
      },

      esimProfileName_: {
        type: String,
        value: '',
        observer: 'onEsimProfileNameChanged_',
      },

      isInputInvalid_: {
        type: Boolean,
        value: false,
      },

      isRenameInProgress_: {
        type: Boolean,
        value: false,
      },
    };
  }

  maxInputLength: number;
  networkState: OncMojo.NetworkStateProperties|null;
  showCellularDisconnectWarning: boolean;
  private errorMessage_: string;
  private esimProfileName_: string;
  private esimProfileRemote_: ESimProfileRemote|null;
  private isInputInvalid_: boolean;
  private isRenameInProgress_: boolean;

  constructor() {
    super();

    this.esimProfileRemote_ = null;
  }

  override connectedCallback(): void {
    super.connectedCallback();

    this.init_();
  }

  private async init_(): Promise<void> {
    if (!(this.networkState &&
          this.networkState.type === NetworkType.kCellular)) {
      return;
    }
    this.esimProfileRemote_ =
        await getESimProfile(this.networkState.typeState.cellular!.iccid);
    // Fail gracefully if init is incomplete, see crbug/1194729.
    if (!this.esimProfileRemote_) {
      this.errorMessage_ = this.i18n('eSimRenameProfileDialogError');
    }
    this.esimProfileName_ = this.networkState.name;

    if (!this.errorMessage_) {
      this.shadowRoot!.querySelector<CrInputElement>(
                          '#eSimprofileName')!.focus();
    }
  }

  /**
   * @param {Event} event
   * @private
   */
  private async onRenameDialogDoneClick_(): Promise<void> {
    if (this.errorMessage_) {
      this.$.profileRenameDialog.close();
      return;
    }

    this.isRenameInProgress_ = true;

    // The C++ layer uses std::u16string, which use 16 bit characters. JS
    // strings support either 8 or 16 bit characters, and must be converted
    // to an array of 16 bit character codes that match std::u16string.
    const name = stringToMojoString16(this.esimProfileName_);

    const response = await this.esimProfileRemote_!.setProfileNickname(name);
    this.handleSetProfileNicknameResponse_(response.result);
  }

  private handleSetProfileNicknameResponse_(result: ESimOperationResult): void {
    this.isRenameInProgress_ = false;
    if (result === ESimOperationResult.kFailure) {
      const showErrorToastEvent = new CustomEvent('show-error-toast', {
        bubbles: true,
        composed: true,
        detail: this.i18n('eSimRenameProfileDialogError'),
      });
      this.dispatchEvent(showErrorToastEvent);
    }
    this.$.profileRenameDialog.close();
  }

  private onCancelClick_(): void {
    this.$.profileRenameDialog.close();
  }

  /**
   * Observer for esimProfileName_ that sanitizes its value by removing any
   * Emojis and truncating it to MAX_INPUT_LENGTH. This method will be
   * recursively called until esimProfileName_ is fully sanitized.
   */
  private onEsimProfileNameChanged_(_newValue: string, oldValue: string): void {
    if (oldValue) {
      const sanitizedOldValue = oldValue.replace(EMOJI_REGEX_EXP, '');
      // If sanitizedOldValue.length > MAX_INPUT_LENGTH, the user attempted to
      // enter more than the max limit, this method was called and it was
      // truncated, and then this method was called one more time.
      this.isInputInvalid_ = sanitizedOldValue.length > MAX_INPUT_LENGTH;
    } else {
      this.isInputInvalid_ = false;
    }

    // Remove all Emojis from the name.
    const sanitizedProfileName =
        this.esimProfileName_.replace(EMOJI_REGEX_EXP, '');

    // Truncate the name to MAX_INPUT_LENGTH.
    this.esimProfileName_ = sanitizedProfileName.substring(0, MAX_INPUT_LENGTH);
  }

  private getInputInfoClass_(isInputInvalid: boolean): string {
    return isInputInvalid ? 'error' : '';
  }

  /**
   * Returns a formatted string containing the current number of characters
   * entered in the input compared to the maximum number of characters allowed.
   */
  private getInputCountString_(esimProfileName: string): string {
    // minimumIntegerDigits is 2 because we want to show a leading zero if
    // length is less than 10.
    return this.i18n(
        'eSimRenameProfileInputCharacterCount',
        esimProfileName.length.toLocaleString(
            /*locales=*/ undefined, {minimumIntegerDigits: 2}),
        MAX_INPUT_LENGTH.toLocaleString());
  }

  private isDoneButtonDisabled_(
      isRenameInProgress: boolean, esimProfileName: string): boolean {
    if (isRenameInProgress) {
      return true;
    }
    return esimProfileName.length < MIN_INPUT_LENGTH;
  }

  private getDoneBtnA11yLabel_(esimProfileName: string): string {
    return this.i18n('eSimRenameProfileDoneBtnA11yLabel', esimProfileName);
  }
}

declare global {
  interface HTMLElementTagNameMap {
    [EsimRenameDialogElement.is]: EsimRenameDialogElement;
  }
}

customElements.define(EsimRenameDialogElement.is, EsimRenameDialogElement);