Bug 1397090 - Add the ability to mask credit card numbers after they have been unmasked. r=lchang draft
authorScott Wu <scottcwwu@gmail.com>
Fri, 08 Sep 2017 12:11:51 +0800
changeset 662749 3d32dedd57daf75a5bb85efab175e1bc6d735ff0
parent 662738 bda524beac249b64aa36016800502a34073bf35a
child 730968 e774ea40efc824fd3db97e33b1fe9efa83d6c069
push id79188
push userbmo:scwwu@mozilla.com
push dateTue, 12 Sep 2017 03:32:06 +0000
reviewerslchang
bugs1397090
milestone57.0a1
Bug 1397090 - Add the ability to mask credit card numbers after they have been unmasked. r=lchang MozReview-Commit-ID: GIjhhwgsl5b
browser/extensions/formautofill/content/manageCreditCards.xhtml
browser/extensions/formautofill/content/manageDialog.js
browser/extensions/formautofill/locales/en-US/formautofill.properties
browser/extensions/formautofill/test/browser/browser_manageCreditCardsDialog.js
--- a/browser/extensions/formautofill/content/manageCreditCards.xhtml
+++ b/browser/extensions/formautofill/content/manageCreditCards.xhtml
@@ -12,26 +12,26 @@
 </head>
 <body>
   <fieldset>
     <legend data-localization="creditCardsListHeader"/>
     <select id="credit-cards" size="9" multiple="multiple"/>
   </fieldset>
   <div id="controls-container">
     <button id="remove" disabled="disabled" data-localization="remove"/>
-    <button id="show-credit-cards" data-localization="showCreditCards"/>
+    <button id="show-hide-credit-cards" data-localization="showCreditCards"/>
     <button id="add" data-localization="add"/>
     <button id="edit" disabled="disabled" data-localization="edit"/>
   </div>
   <script type="application/javascript">
     "use strict";
     /* global ManageCreditCards */
     new ManageCreditCards({
       records: document.getElementById("credit-cards"),
       controlsContainer: document.getElementById("controls-container"),
       remove: document.getElementById("remove"),
-      showCreditCards: document.getElementById("show-credit-cards"),
+      showHideCreditCards: document.getElementById("show-hide-credit-cards"),
       add: document.getElementById("add"),
       edit: document.getElementById("edit"),
     });
   </script>
 </body>
 </html>
--- a/browser/extensions/formautofill/content/manageDialog.js
+++ b/browser/extensions/formautofill/content/manageDialog.js
@@ -23,17 +23,16 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 this.log = null;
 FormAutofillUtils.defineLazyLogGetter(this, "manageAddresses");
 
 class ManageRecords {
   constructor(subStorageName, elements) {
     this._storageInitPromise = profileStorage.initialize();
     this._subStorageName = subStorageName;
     this._elements = elements;
-    this._records = [];
     this._newRequest = false;
     this._isLoadingRecords = false;
     this.prefWin = window.opener;
     this.localizeDocument();
     window.addEventListener("DOMContentLoaded", this, {once: true});
   }
 
   async init() {
@@ -147,16 +146,17 @@ class ManageRecords {
     // Pause listening to storage change event to avoid triggering `loadRecords`
     // when removing records
     Services.obs.removeObserver(this, "formautofill-storage-changed");
 
     for (let option of options) {
       storage.remove(option.value);
       option.remove();
     }
+    this.updateButtonsStates(this._selectedOptions);
 
     // Resume listening to storage change event
     Services.obs.addObserver(this, "formautofill-storage-changed");
     // For testing only: notify record(s) has been removed
     this._elements.records.dispatchEvent(new CustomEvent("RecordsRemoved"));
   }
 
   /**
@@ -322,31 +322,32 @@ class ManageAddresses extends ManageReco
     }
     return parts.join(", ");
   }
 }
 
 class ManageCreditCards extends ManageRecords {
   constructor(elements) {
     super("creditCards", elements);
-    this.hasMasterPassword = MasterPassword.isEnabled;
-    if (this.hasMasterPassword) {
-      elements.showCreditCards.setAttribute("hidden", true);
+    this._hasMasterPassword = MasterPassword.isEnabled;
+    this._isDecrypted = false;
+    if (this._hasMasterPassword) {
+      elements.showHideCreditCards.setAttribute("hidden", true);
     }
   }
 
   /**
    * Open the edit address dialog to create/edit a credit card.
    *
    * @param  {object} creditCard [optional]
    */
   async openEditDialog(creditCard) {
     // If master password is set, ask for password if user is trying to edit an
     // existing credit card.
-    if (!this.hasMasterPassword || !creditCard || await MasterPassword.prompt()) {
+    if (!this._hasMasterPassword || !creditCard || await MasterPassword.prompt()) {
       this.prefWin.gSubDialog.open(EDIT_CREDIT_CARD_URL, null, creditCard);
     }
   }
 
   /**
    * Get credit card display label. It should display masked numbers and the
    * cardholder's name, separated by a comma. If `showCreditCards` is set to
    * true, decrypted credit card numbers are shown instead.
@@ -368,23 +369,51 @@ class ManageCreditCards extends ManageRe
       parts.push(ccLabel);
     }
     if (creditCard["cc-name"]) {
       parts.push(creditCard["cc-name"]);
     }
     return parts.join(", ");
   }
 
-  async decryptOptions(options) {
+  async toggleShowHideCards(options) {
+    this._isDecrypted = !this._isDecrypted;
+    this.updateShowHideButtonState();
+    await this.updateLabels(options, this._isDecrypted);
+  }
+
+  async updateLabels(options, isDecrypted) {
     for (let option of options) {
-      option.text = await this.getLabel(option.record, true);
+      option.text = await this.getLabel(option.record, isDecrypted);
     }
-    // For testing only: Notify when credit cards have been decrypted
-    this._elements.records.dispatchEvent(new CustomEvent("OptionsDecrypted"));
+    // For testing only: Notify when credit cards labels have been updated
+    this._elements.records.dispatchEvent(new CustomEvent("LabelsUpdated"));
+  }
+
+  async renderRecordElements(records) {
+    // Revert back to encrypted form when re-rendering happens
+    this._isDecrypted = false;
+    await super.renderRecordElements(records);
+  }
+
+  updateButtonsStates(selectedCount) {
+    this.updateShowHideButtonState();
+    super.updateButtonsStates(selectedCount);
+  }
+
+  updateShowHideButtonState() {
+    if (this._elements.records.length) {
+      this._elements.showHideCreditCards.removeAttribute("disabled");
+    } else {
+      this._elements.showHideCreditCards.setAttribute("disabled", true);
+    }
+    this._elements.showHideCreditCards.textContent =
+      this._isDecrypted ? FormAutofillUtils.stringBundle.GetStringFromName("hideCreditCards") :
+                          FormAutofillUtils.stringBundle.GetStringFromName("showCreditCards");
   }
 
   handleClick(event) {
-    if (event.target == this._elements.showCreditCards) {
-      this.decryptOptions(this._elements.records.options);
+    if (event.target == this._elements.showHideCreditCards) {
+      this.toggleShowHideCards(this._elements.records.options);
     }
     super.handleClick(event);
   }
 }
--- a/browser/extensions/formautofill/locales/en-US/formautofill.properties
+++ b/browser/extensions/formautofill/locales/en-US/formautofill.properties
@@ -41,16 +41,17 @@ fieldNameSeparator = ,\u0020
 phishingWarningMessage = Also autofills %S
 phishingWarningMessage2 = Autofills %S
 
 manageAddressesTitle = Saved Addresses
 manageCreditCardsTitle = Saved Credit Cards
 addressesListHeader = Addresses
 creditCardsListHeader = Credit Cards
 showCreditCards = Show Credit Cards
+hideCreditCards = Hide Credit Cards
 remove = Remove
 add = Add…
 edit = Edit…
 
 addNewAddressTitle = Add New Address
 editAddressTitle = Edit Address
 givenName = First Name
 additionalName = Middle Name
--- a/browser/extensions/formautofill/test/browser/browser_manageCreditCardsDialog.js
+++ b/browser/extensions/formautofill/test/browser/browser_manageCreditCardsDialog.js
@@ -1,34 +1,34 @@
 "use strict";
 
 Cu.import("resource://testing-common/LoginTestUtils.jsm", this);
 
 const TEST_SELECTORS = {
   selRecords: "#credit-cards",
   btnRemove: "#remove",
-  btnShowCreditCards: "#show-credit-cards",
+  btnShowHideCreditCards: "#show-hide-credit-cards",
   btnAdd: "#add",
   btnEdit: "#edit",
 };
 
 const DIALOG_SIZE = "width=600,height=400";
 
 add_task(async function test_manageCreditCardsInitialState() {
   await BrowserTestUtils.withNewTab({gBrowser, url: MANAGE_CREDIT_CARDS_DIALOG_URL}, async function(browser) {
     await ContentTask.spawn(browser, TEST_SELECTORS, (args) => {
       let selRecords = content.document.querySelector(args.selRecords);
       let btnRemove = content.document.querySelector(args.btnRemove);
-      let btnShowCreditCards = content.document.querySelector(args.btnShowCreditCards);
+      let btnShowHideCreditCards = content.document.querySelector(args.btnShowHideCreditCards);
       let btnAdd = content.document.querySelector(args.btnAdd);
       let btnEdit = content.document.querySelector(args.btnEdit);
 
       is(selRecords.length, 0, "No credit card");
       is(btnRemove.disabled, true, "Remove button disabled");
-      is(btnShowCreditCards.disabled, false, "Show Credit Cards button disabled");
+      is(btnShowHideCreditCards.disabled, true, "Show Credit Cards button disabled");
       is(btnAdd.disabled, false, "Add button enabled");
       is(btnEdit.disabled, true, "Edit button disabled");
     });
   });
 });
 
 add_task(async function test_cancelManageCreditCardsDialogWithESC() {
   await new Promise(resolve => {
@@ -99,46 +99,72 @@ add_task(async function test_showCreditC
   await saveCreditCard(TEST_CREDIT_CARD_1);
   await saveCreditCard(TEST_CREDIT_CARD_2);
   await saveCreditCard(TEST_CREDIT_CARD_3);
 
   let win = window.openDialog(MANAGE_CREDIT_CARDS_DIALOG_URL, null, DIALOG_SIZE);
   await BrowserTestUtils.waitForEvent(win, "FormReady");
 
   let selRecords = win.document.querySelector(TEST_SELECTORS.selRecords);
-  let btnShowCreditCards = win.document.querySelector(TEST_SELECTORS.btnShowCreditCards);
+  let btnShowHideCreditCards = win.document.querySelector(TEST_SELECTORS.btnShowHideCreditCards);
 
-  EventUtils.synthesizeMouseAtCenter(btnShowCreditCards, {}, win);
-  await BrowserTestUtils.waitForEvent(selRecords, "OptionsDecrypted");
+  is(btnShowHideCreditCards.disabled, false, "Show credit cards button enabled");
+  is(btnShowHideCreditCards.textContent, "Show Credit Cards", "Label should be 'Show Credit Cards'");
 
+  // Show credit card numbers
+  EventUtils.synthesizeMouseAtCenter(btnShowHideCreditCards, {}, win);
+  await BrowserTestUtils.waitForEvent(selRecords, "LabelsUpdated");
   is(selRecords[0].text, "9999888877776666", "Decrypted credit card 3");
   is(selRecords[1].text, "1111222233334444, Timothy Berners-Lee", "Decrypted credit card 2");
   is(selRecords[2].text, "1234567812345678, John Doe", "Decrypted credit card 1");
+  is(btnShowHideCreditCards.textContent, "Hide Credit Cards", "Label should be 'Hide Credit Cards'");
 
+  // Hide credit card numbers
+  EventUtils.synthesizeMouseAtCenter(btnShowHideCreditCards, {}, win);
+  await BrowserTestUtils.waitForEvent(selRecords, "LabelsUpdated");
+  is(selRecords[0].text, "**** 6666", "Masked credit card 3");
+  is(selRecords[1].text, "**** 4444, Timothy Berners-Lee", "Masked credit card 2");
+  is(selRecords[2].text, "**** 5678, John Doe", "Masked credit card 1");
+  is(btnShowHideCreditCards.textContent, "Show Credit Cards", "Label should be 'Show Credit Cards'");
+
+  // Show credit card numbers again to test if they revert back to masked form when reloaded
+  EventUtils.synthesizeMouseAtCenter(btnShowHideCreditCards, {}, win);
+  await BrowserTestUtils.waitForEvent(selRecords, "LabelsUpdated");
+  // Ensure credit card numbers are shown again
+  is(selRecords[0].text, "9999888877776666", "Decrypted credit card 3");
+  // Remove a card to trigger reloading
   await removeCreditCards([selRecords.options[2].value]);
+  await BrowserTestUtils.waitForEvent(selRecords, "RecordsLoaded");
+  is(selRecords[0].text, "**** 6666", "Masked credit card 3");
+  is(selRecords[1].text, "**** 4444, Timothy Berners-Lee", "Masked credit card 2");
+
+  // Remove the rest of the cards
   await removeCreditCards([selRecords.options[1].value]);
   await removeCreditCards([selRecords.options[0].value]);
+  await BrowserTestUtils.waitForEvent(selRecords, "RecordsLoaded");
+  is(btnShowHideCreditCards.disabled, true, "Show credit cards button is disabled when there is no card");
+
   win.close();
 });
 
 add_task(async function test_hasMasterPassword() {
   await saveCreditCard(TEST_CREDIT_CARD_1);
   LoginTestUtils.masterPassword.enable();
 
   let win = window.openDialog(MANAGE_CREDIT_CARDS_DIALOG_URL, null, DIALOG_SIZE);
   await BrowserTestUtils.waitForEvent(win, "FormReady");
 
   let selRecords = win.document.querySelector(TEST_SELECTORS.selRecords);
   let btnRemove = win.document.querySelector(TEST_SELECTORS.btnRemove);
-  let btnShowCreditCards = win.document.querySelector(TEST_SELECTORS.btnShowCreditCards);
+  let btnShowHideCreditCards = win.document.querySelector(TEST_SELECTORS.btnShowHideCreditCards);
   let btnAdd = win.document.querySelector(TEST_SELECTORS.btnAdd);
   let btnEdit = win.document.querySelector(TEST_SELECTORS.btnEdit);
   let masterPasswordDialogShown = waitForMasterPasswordDialog();
 
-  is(btnShowCreditCards.hidden, true, "Show credit cards button is hidden");
+  is(btnShowHideCreditCards.hidden, true, "Show credit cards button is hidden");
 
   // Master password dialog should show when trying to edit a credit card record.
   EventUtils.synthesizeMouseAtCenter(selRecords.children[0], {}, win);
   EventUtils.synthesizeMouseAtCenter(btnEdit, {}, win);
   await masterPasswordDialogShown;
 
   // Master password is not required for removing credit cards.
   EventUtils.synthesizeMouseAtCenter(btnRemove, {}, win);