Source: panels/simpin_dialog/simpin_dialog.js

/**
 * SimPinDialog is a constructor function that can help us process
 * any simpin related works like enable/disable lock, change pins ... etc.
 *
 * @module @SimPinDialog
 */
define(function(require) {
  'use strict';

  var SimSecurity = require('modules/sim_security');
  var DialogService = require('modules/dialog_service');
  var l10n = window.navigator.mozL10n;

  function SimPinDialog(elements) {
    this._localize = l10n.setAttributes;
    this._elements = elements;
    this._method = '';
    this._mode = '';
    this._cardIndex = 0;
    this._pinOptions = {};
    this._allowedRetryCounts = {
      pin: 3,
      pin2: 3,
      puk: 10,
      puk2: 10
    };
  }

  SimPinDialog.prototype = {
    /**
     * init function
     *
     * @memberOf SimPinDialog
     * @access public
     * @param {Object} options
     * @param {String} options.method - lock, unlock ... etc
     * @param {Number} options.cardIndex - which simcard
     * @param {Object} options.pinOptions - extra arguments for icc operations
     */
    init: function(options) {
      this._method = options.method;
      this._cardIndex = options.cardIndex;
      this._pinOptions = options.pinOptions;
      this._bindInputClickEvent();
      this._initUI();
    },

    /**
     * We will call this function when users click on submit button to make sure
     * DialogService can do the right thing.
     *
     * @memberOf SimPinDialog
     * @access public
     * @return {Promise}
     */
    verify: function() {
      switch (this._method) {
        // unlock SIM
        case 'unlock_pin':
          return this._unlockPin();
        case 'unlock_puk':
          return this._unlockPuk('puk');
        case 'unlock_puk2':
          return this._unlockPuk('puk2');

        // PIN lock
        case 'enable_lock':
          return this._enableLock(true);
        case 'disable_lock':
          return this._enableLock(false);
        case 'change_pin':
          return this._changePin('pin');

        // get PIN2 code (FDN contact list)
        case 'get_pin2':
          return this._updateFdnContact();

        // PIN2 lock (FDN)
        case 'enable_fdn':
          return this._enableFdn(true);
        case 'disable_fdn':
          return this._enableFdn(false);
        case 'change_pin2':
          return this._changePin('pin2');

        default:
          return Promise.reject();
      }
    },

    /**
     * We will clear all pre-set values when the dialog is hidden.
     *
     * @memberOf SimPinDialog
     * @access public
     */
    clear: function() {
      this._elements.errorMsg.hidden = true;
      this._elements.pinInput.value = '';
      this._elements.pukInput.value = '';
      this._elements.newPinInput.value = '';
      this._elements.confirmPinInput.value = '';
    },

    /**
     * Unlock pin
     *
     * @memberOf SimPinDialog
     * @access private
     * @return {Promise}
     */
    _unlockPin: function() {
      var pin = this._elements.pinInput.value;
      if (pin === '') {
        return Promise.reject();
      }
      return this._unlockCardLock({
        lockType: 'pin',
        pin: pin
      });
    },

    /**
     * Unlock puk1 or puk2
     *
     * @memberOf SimPinDialog
     * @param {String} lockType
     * @access private
     * @return {Promise}
     */
    _unlockPuk: function(lockType) {
      lockType = lockType || 'puk';
      var puk = this._elements.pukInput.value;
      var newPin = this._elements.newPinInput.value;
      var confirmPin = this._elements.confirmPinInput.value;

      if (puk === '' || newPin === '' || confirmPin === '') {
        return Promise.reject();
      }

      if (newPin !== confirmPin) {
        this._showMessage('newPinErrorMsg');
        this._elements.newPinInput.value = '';
        this._elements.confirmPinInput.value = '';
        this._elements.pukInput.value = '';
        this._elements.pukInput.focus();
        return Promise.reject();
      }

      return this._unlockCardLock({
        lockType: lockType,
        puk: puk,
        newPin: newPin
      });
    },

    /**
     * This is an internal function to pass all arguments to icc and waiting
     * for returned results.
     *
     * @memberOf SimPinDialog
     * @param {Object} options
     * @param {String} options.pin
     * @param {String} options.puk
     * @param {String} options.newPin
     * @access private
     * @return {Promise}
     */
    _unlockCardLock: function(options) {
      return SimSecurity.unlockCardLock(this._cardIndex, options).then(() => {
        // do nothing
      }, (error) => {
        var needToCloseDialog = this._handleCardLockError({
          lockType: options.lockType,
          error: error
        });
        if (!needToCloseDialog) {
          return Promise.reject();
        }
      });
    },

    /**
     * Lock pin
     *
     * @memberOf SimPinDialog
     * @param {Boolean} enabled
     * @access private
     * @return {Promise}
     */
    _enableLock: function(enabled) {
      var pin = this._elements.pinInput.value;
      if (pin === '') {
        return Promise.reject();
      }
      return this._setCardLock({
        lockType: 'pin',
        pin: pin,
        enabled: enabled
      });
    },

    /**
     * Enable FDN on Simcards and related Call functions under its own panel.
     *
     * @memberOf SimPinDialog
     * @param {Boolean} enabled
     * @access private
     * @return {Promise}
     */
    _enableFdn: function(enabled) {
      var pin = this._elements.pinInput.value;
      if (pin === '') {
        return Promise.reject();
      }
      return this._setCardLock({
        lockType: 'fdn',
        pin2: pin,
        enabled: enabled
      });
    },

    /**
     * Change the pre-defined codes in the simcard.
     *
     * @memberOf SimPinDialog
     * @param {String} lockType
     * @access private
     * @return {Promise}
     */
    _changePin: function(lockType) {
      // lockType = `pin' or `pin2'
      lockType = lockType || 'pin';
      var pin = this._elements.pinInput.value;
      var newPin = this._elements.newPinInput.value;
      var confirmPin = this._elements.confirmPinInput.value;

      if (pin === '' || newPin === '' || confirmPin === '') {
        return Promise.reject();
      }

      if (newPin !== confirmPin) {
        this._showMessage('newPinErrorMsg');
        this._elements.newPinInput.value = '';
        this._elements.confirmPinInput.value = '';
        this._elements.pinInput.value = '';
        this._elements.pinInput.focus();
        return Promise.reject();
      }

      return this._setCardLock({
        lockType: lockType,
        pin: pin,
        newPin: newPin
      });
    },

    /**
     * This is an internal function to pass all arguments to icc and waiting
     * for returned results.
     *
     * @memberOf SimPinDialog
     * @param {Object} options
     * @param {String} options.lockType
     * @param {String} options.pin
     * @param {String} options.newPin
     * @param {String} options.pin2
     * @param {Boolean} options.enabled
     * @access private
     * @return {Promise}
     */
    _setCardLock: function(options) {
      return SimSecurity.setCardLock(this._cardIndex, options).then(() => {
        // do nothing
      }, (error) => {
        var needToCloseDialog = this._handleCardLockError({
          lockType: options.lockType,
          error: error
        });
        if (!needToCloseDialog) {
          return Promise.reject();
        }
      });
    },

    /**
     * this function is used when we are going to update fdn contacts under
     * call panel.
     *
     * @memberOf SimPinDialog
     * @access private
     * @return {Promise}
     */
    _updateFdnContact: function() {
      //  Updates a FDN contact. For some reason, `icc.updateContact` requires
      //  the  pin input value instead of delegating to `icc.setCardLock`.
      //  That means that, in case of failure, the error is different that the
      //  one that `icc.setCardLock` gives. This means that we have to handle
      //  it separatedly instead of being able to use the existing
      //  `handleCardLockError` above.
      //
      //  Among other things, it doesn't include the retryCount, so we can't
      //  tell the user how many remaining tries she has. What a mess.
      //  This should be solved when bug 1070941 is fixed.

      var fdnContact = this._pinOptions.fdnContact;
      return SimSecurity.updateContact(this._cardIndex, 'fdn', fdnContact,
        this._elements.pinInput.value).then(() => {
          return fdnContact;
      }, (error) => {
        switch (error.name) {
          case 'IncorrectPassword':
          case 'SimPin2':
            // TODO: count retries (not supported by the platform) ->
            // Bug 1070941
            this._initUI('get_pin2');
            this._showMessage('fdnErrorMsg');
            this._elements.pinInput.value = '';
            this._elements.pinInput.focus();
            return Promise.reject();

          case 'SimPuk2':
            this._initUI('unlock_puk2');
            this._elements.pukInput.focus();
            return Promise.reject();

          case 'NoFreeRecordFound':
            DialogService.alert('fdnNoFDNFreeRecord');
            return fdnContact;

          default:
            console.error('Could not edit FDN contact on SIM card - ', error);
            return fdnContact;
        }
      });
    },

    /**
     * We will try to handle all returned errors here when requests are
     * returned from icc.
     *
     * @memberOf SimPinDialog
     * @param {Object} options
     * @param {String} options.lockType
     * @param {Object} options.error
     * @param {Number} options.error.retryCount
     * @param {String} options.error.name
     * @access private
     * @return {Boolean} - true means close dialog, others mean not
     */
    _handleCardLockError: function(options) {
      var error = options.error;
      var lockType = options.lockType;
      var retryCount = error.retryCount;
      var errorName = error.name;

      // expected: 'pin', 'fdn', 'puk'
      if (!lockType) {
        // we don't know what's going on here, we have close the dialog.
        console.error('`handleCardLockError` called without a lockType. ' +
          'This should never even happen.', error);
        return true;
      }

      switch (errorName) {
        case 'SimPuk2':
        case 'IncorrectPassword':
          return this._handleRetryPassword(lockType, retryCount);

        default:
          DialogService.alert('genericLockError');
          console.error('Error of type ' + errorName +
            ' happened coming from an IccCardLockError event', error);
          return true;
      }
    },

    /**
     * We will handle all retry cases here to make sure we can change to
     * the right UI for users to continue.
     *
     * @memberOf SimPinDialog
     * @param {String} lockType
     * @param {Number} retryCount
     * @access private
     * @return {Boolean} - true means close dialog, others mean not
     */
    _handleRetryPassword: function(lockType, retryCount) {
      // after three strikes, ask for PUK/PUK2
      if (retryCount <= 0) {
        if (lockType === 'pin') {
          // we leave this for system app, so let's close the dialog
          return true;
        } else if (lockType === 'fdn' || lockType === 'pin2') {
          this._initUI('unlock_puk2');
          this._elements.pukInput.focus();
          return false;
        } else {
          // out of PUK/PUK2: we're doomed
          DialogService.alert('genericLockError');
          return true;
        }
      } else {
        // We still have retryCount, let users input values again
        var msgId = (retryCount > 1) ? 'AttemptMsg3' : 'LastChanceMsg';
        this._showMessage(lockType + 'ErrorMsg', lockType + msgId, {
          n: retryCount
        });
        this._showRetryCount(retryCount);

        if (lockType === 'pin' || lockType === 'fdn') {
          this._elements.pinInput.value = '';
          this._elements.pinInput.focus();
        } else if (lockType === 'puk') {
          this._elements.pukInput.value = '';
          this._elements.pukInput.focus();
        } else if (lockType === 'puk2') {
          this._elements.pinInput.value = '';
          this._elements.pukInput.value = '';
          this._elements.pukInput.focus();
        }

        return false;
      }
    },

    /**
     * we will use `mode` to decide what kind of UI should be shown/hidden.
     *
     * @memberOf SimPinDialog
     * @param {String} mode
     * @access private
     */
    _setMode: function(mode) {
      this._mode = mode;
      this._elements.pinArea.hidden = (mode === 'puk');
      this._elements.pukArea.hidden = (mode !== 'puk');
      this._elements.newPinArea.hidden =
        this._elements.confirmPinArea.hidden = (mode === 'pin');
    },

    /**
     * We can show any message on the screen.
     *
     * @memberOf SimPinDialog
     * @param {String} headerL10nId
     * @param {String} bodyL10nId
     * @param {Object} args
     * @access private
     */
    _showMessage: function(headerL10nId, bodyL10nId, args) {
      if (!headerL10nId) {
        this._elements.errorMsg.hidden = true;
        return;
      }

      this._elements.errorMsgHeader.setAttribute('data-l10n-id', headerL10nId);
      this._localize(this._elements.errorMsgBody, bodyL10nId, args);
      this._elements.errorMsg.hidden = false;
    },

    /**
     * To hint users about how many times they can retry before the simcard
     * got locked.
     *
     * @memberOf SimPinDialog
     * @param {Number} retryCount
     * @access private
     */
    _showRetryCount: function(retryCount) {
      if (!retryCount) {
        this._elements.triesLeftMsg.hidden = true;
      } else {
        this._localize(this._elements.triesLeftMsg, 'inputCodeRetriesLeft', {
          n: retryCount
        });
        this._elements.triesLeftMsg.hidden = false;
      }
    },

    /**
     * bind onclick event on all inputs
     *
     * @memberOf SimPinDialog
     * @access private
     */
    _bindInputClickEvent: function() {
      var elements = this._elements;
      var inputs = [
        'pinInput',
        'pukInput',
        'newPinInput',
        'confirmPinInput'
      ];

      inputs.forEach((inputName) => {
        var input = elements[inputName];
        input.oninput = () => {
          this._updateDoneButtonState();
        };
      });
    },

    /**
     * We have to control `done` button in each condition to make sure
     * its state is right based on user's input
     *
     * @memberOf SimPinDialog
     * @access private
     */
    _updateDoneButtonState: function() {
      var elements = this._elements;
      switch (this._mode) {
        case 'change_pin':
          if (elements.pinInput.value.length < 4 ||
            elements.newPinInput.value.length < 4 ||
            elements.confirmPinInput.value.length < 4 ||
            elements.newPinInput.value.length !==
            elements.confirmPinInput.value.length) {
              elements.dialogDone.disabled = true;
          } else {
            elements.dialogDone.disabled = false;
          }
          break;

        case 'pin':
          elements.dialogDone.disabled = (elements.pinInput.value.length < 4);
          break;

        case 'puk':
          elements.dialogDone.disabled = (elements.pukInput.value.length < 4);
          break;

        default:
          console.error('we should not jump to this condition');
          break;
      }
    },

    /**
     * We will change the UI based on current lockType.
     *
     * @memberOf SimPinDialog
     * @access private
     */
    _initUI: function(method) {
      if (method) {
        this._method = method;
      }

      this._showMessage();
      this._showRetryCount(); // Clear the retry count at first
      this._elements.dialogDone.disabled = true;

      var lockType = 'pin'; // used to query the number of retries left
      switch (this._method) {
        // get PIN code
        case 'get_pin2':
          lockType = 'pin2';
          this._setMode('pin');
          this._localize(this._elements.pinArea.querySelector('div'),
            'simPin2');
          this._localize(this._elements.dialogTitle, lockType + 'Title');
          break;

        // unlock SIM
        case 'unlock_pin':
          this._setMode('pin');
          this._localize(this._elements.pinArea.querySelector('div'),
            'simPin');
          this._localize(this._elements.dialogTitle, 'pinTitle');
          break;

        case 'unlock_puk':
          lockType = 'puk';
          this._setMode('puk');
          this._showMessage('simCardLockedMsg', 'enterPukMsg');
          this._localize(this._elements.pukArea.querySelector('div'),
            'pukCode');
          this._localize(this._elements.dialogTitle, 'pukTitle');
          break;

        case 'unlock_puk2':
          lockType = 'puk2';
          this._setMode('puk');
          this._showMessage('simCardLockedMsg', 'enterPuk2Msg');
          this._localize(this._elements.pukArea.querySelector('div'),
            'puk2Code');
          this._localize(this._elements.dialogTitle, 'puk2Title');
          this._localize(this._elements.newPinArea.querySelector('div'),
            'newSimPin2Msg');
          this._localize(this._elements.confirmPinArea.querySelector('div'),
            'confirmNewSimPin2Msg');
          break;

        // PIN lock
        case 'enable_lock':
          this._setMode('pin');
          this._localize(this._elements.pinArea.querySelector('div'),
            'simPin');
          this._localize(this._elements.dialogTitle, 'pinTitle');
          break;

        case 'disable_lock':
          this._setMode('pin');
          this._localize(this._elements.pinArea.querySelector('div'),
            'simPin');
          this._localize(this._elements.dialogTitle, 'pinTitle');
          break;

        case 'change_pin':
          this._setMode('change_pin');
          this._localize(this._elements.pinArea.querySelector('div'),
            'simPin');
          this._localize(this._elements.newPinArea.querySelector('div'),
            'newSimPinMsg');
          this._localize(this._elements.confirmPinArea.querySelector('div'),
            'confirmNewSimPinMsg');
          this._localize(this._elements.dialogTitle, 'newpinTitle');
          break;

        // FDN lock (PIN2)
        case 'enable_fdn':
          lockType = 'pin2';
          this._setMode('pin');
          this._localize(this._elements.pinArea.querySelector('div'),
            'simPin2');
          this._localize(this._elements.dialogTitle, 'fdnEnable');
          break;

        case 'disable_fdn':
          lockType = 'pin2';
          this._setMode('pin');
          this._localize(this._elements.pinArea.querySelector('div'),
            'simPin2');
          this._localize(this._elements.dialogTitle, 'fdnDisable');
          break;

        case 'change_pin2':
          lockType = 'pin2';
          this._setMode('change_pin');
          this._localize(this._elements.pinArea.querySelector('div'),
            'simPin2');
          this._localize(this._elements.newPinArea.querySelector('div'),
            'newSimPin2Msg');
          this._localize(this._elements.confirmPinArea.querySelector('div'),
            'confirmNewSimPin2Msg');
          this._localize(this._elements.dialogTitle, 'fdnReset');
          break;

        // unsupported
        default:
          console.error('unsupported "' + this._method + '" method');
          break;
      }

      // display the number of remaining retries if necessary
      // XXX this only works with the emulator
      // (and some commercial RIL stacks...)
      // https://bugzilla.mozilla.org/show_bug.cgi?id=905173
      SimSecurity.getCardLockRetryCount(this._cardIndex, lockType)
        .then((result) => {
          var retryCount = result.retryCount;
          if (retryCount === this._allowedRetryCounts[lockType]) {
            // hide the retry count if users had not input incorrect codes
            retryCount = null;
          }
          this._showRetryCount(retryCount);
      });
    }
  };

  return function ctor_simPinDialog(elements) {
    return new SimPinDialog(elements);
  };
});