chromium/ash/webui/common/resources/cellular_setup/provisioning_page.ts

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

/**
 * Carrier Provisioning subpage in Cellular Setup flow. This element contains a
 * webview element that loads the carrier's provisioning portal. It also has an
 * error state that displays a message for errors that may happen during this
 * step.
 */
import './base_page.js';
import '//resources/ash/common/cr_elements/cr_hidden_style.css.js';
import '//resources/polymer/v3_0/iron-flex-layout/iron-flex-layout-classes.js';
import '//resources/polymer/v3_0/paper-spinner/paper-spinner-lite.js';

import {I18nMixin} from '//resources/ash/common/cr_elements/i18n_mixin.js';
import {assert} from '//resources/js/assert.js';
import {CellularMetadata} from '//resources/mojo/chromeos/ash/services/cellular_setup/public/mojom/cellular_setup.mojom-webui.js';
import {PolymerElement} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import {CellularSetupDelegate} from './cellular_setup_delegate.js';
import {getTemplate} from './provisioning_page.html.js';
import {postDeviceDataToWebview} from './webview_post_util.js';

const ProvisioningPageElementBase = I18nMixin(PolymerElement);

export interface ProvisioningPageElement {
  $: {
    portalContainer: HTMLDivElement,
  };
}

export class ProvisioningPageElement extends ProvisioningPageElementBase {
  static get is() {
    return 'provisioning-page' as const;
  }

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

  static get properties() {
    return {
      delegate: Object,

      /**
       * Whether error state should be shown.
       */
      showError: {
        type: Boolean,
        value: false,
        notify: true,
      },

      /**
       * Metadata used to open carrier provisioning portal. Expected to start as
       * null, then change to a valid object.
       */
      cellularMetadata: {
        type: Object,
        value: null,
        observer: 'onCellularMetadataChanged_',
      },

      /**
       * Whether the carrier portal has completed being loaded.
       */
      hasCarrierPortalLoaded_: {
        type: Boolean,
        value: false,
      },

      /**
       * The last carrier name provided via |cellularMetadata|.
       */
      carrierName_: {
        type: String,
        value: '',
      },
    };
  }

  delegate: CellularSetupDelegate;
  showError: boolean;
  cellularMetadata: CellularMetadata|null;
  private hasCarrierPortalLoaded_: boolean;
  private carrierName_: string;

  private getPageTitle_(): string|null {
    if (!this.delegate.shouldShowPageTitle()) {
      return null;
    }
    if (this.showError) {
      return this.i18n('provisioningPageErrorTitle', this.carrierName_);
    }
    if (this.hasCarrierPortalLoaded_) {
      return this.i18n('provisioningPageActiveTitle');
    }
    return this.i18n('provisioningPageLoadingTitle', this.carrierName_);
  }

  private getPageMessage_(): string|null {
    if (this.showError) {
      return this.i18n('provisioningPageErrorMessage', this.carrierName_);
    }
    return null;
  }

  private shouldShowSpinner_(): boolean {
    return !this.showError && !this.hasCarrierPortalLoaded_;
  }

  private shouldShowPortal_(): boolean {
    return !this.showError && this.hasCarrierPortalLoaded_;
  }

  private getPortalWebview(): chrome.webviewTag.WebView|null {
    return this.shadowRoot!.querySelector('webview');
  }

  private onCellularMetadataChanged_(): void {
    // Once |cellularMetadata| has been set, load the carrier provisioning page.
    if (this.cellularMetadata) {
      this.carrierName_ = this.cellularMetadata.carrier;
      this.loadPortal_();
      return;
    }

    // If |cellularMetadata| is now null, the page should be reset so that a new
    // attempt can begin.
    this.resetPage_();
  }

  private loadPortal_(): void {
    assert(!!this.cellularMetadata);
    assert(!this.getPortalWebview());

    const portalWebview =
        (document.createElement('webview')) as chrome.webviewTag.WebView;
    this.$.portalContainer.appendChild(portalWebview);

    portalWebview.addEventListener(
        'loadabort', this.onPortalLoadAbort_.bind(this));
    portalWebview.addEventListener(
        'loadstop', this.onPortalLoadStop_.bind(this));
    window.addEventListener('message', this.onMessageReceived_.bind(this));

    // Setting a <webview>'s "src" attribute triggers a GET request, but some
    // carrier portals require a POST request instead. If data is provided for a
    // POST request body, use a utility function to load the webview.
    if (this.cellularMetadata.paymentPostData) {
      postDeviceDataToWebview(
          portalWebview, this.cellularMetadata.paymentUrl.url,
          this.cellularMetadata.paymentPostData);
      return;
    }

    // Otherwise, use a normal GET request by specifying the "src".
    portalWebview.src = this.cellularMetadata.paymentUrl.url;
  }

  private resetPage_(): void {
    this.hasCarrierPortalLoaded_ = false;

    // Remove the portal from the DOM if it exists.
    const portalWebview = this.getPortalWebview();
    if (portalWebview) {
      portalWebview.remove();
    }
  }

  private onPortalLoadAbort_(): void {
    this.showError = true;
  }

  private onPortalLoadStop_(): void {
    if (this.hasCarrierPortalLoaded_) {
      return;
    }

    this.hasCarrierPortalLoaded_ = true;
    this.dispatchEvent(new CustomEvent(
        'carrier-portal-loaded', {bubbles: true, composed: true}));

    // When the portal loads, it expects to receive a message from this frame
    // alerting it that loading has completed successfully.
    const portalWebview = this.getPortalWebview();
    assert(!!portalWebview);
    const contentWindow = portalWebview.contentWindow;
    assert(!!contentWindow);
    contentWindow.postMessage(
        {msg: 'loadedInWebview'}, this.cellularMetadata!.paymentUrl?.url);
  }

  private onMessageReceived_(event: MessageEvent) {
    const messageType = (event.data.type) as string;
    const status = (event.data.status) as string;

    // The <webview> requested information about this device. Reply by posting a
    // message back to it.
    if (messageType === 'requestDeviceInfoMsg') {
      const portalWebview = this.getPortalWebview();
      assert(!!portalWebview);
      const contentWindow = portalWebview.contentWindow;
      assert(!!contentWindow);
      contentWindow.postMessage(
          {
            carrier: this.cellularMetadata!.carrier,
            MEID: this.cellularMetadata!.meid,
            IMEI: this.cellularMetadata!.imei,
            MDN: this.cellularMetadata!.mdn,
          },
          this.cellularMetadata!.paymentUrl?.url);
      return;
    }

    // The <webview> provided an update on the status of the activation attempt.
    if (messageType === 'reportTransactionStatusMsg') {
      const success = status === 'ok';
      this.dispatchEvent(new CustomEvent('on-carrier-portal-result', {
          bubbles: true, composed: true, detail: success}));
      return;
    }
  }
}

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

customElements.define(ProvisioningPageElement.is, ProvisioningPageElement);