Bug 1477105 - Support cc-type as valid field for credit cards in form autofill. r=MattN
authorSam Foster <sfoster@mozilla.com>
Tue, 28 Aug 2018 15:59:57 -0700
changeset 488831 1f763bb7c0efd5b8e85d6c19ad8d7c267011239e
parent 488830 ba1272b8b6390e8413e5d54f500914b1a95d72ba
child 488832 ac43fbec9205aa4cd62d8b1d92393b210e87b205
push id9734
push usershindli@mozilla.com
push dateThu, 30 Aug 2018 12:18:07 +0000
treeherdermozilla-beta@71c71ab3afae [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersMattN
bugs1477105
milestone63.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1477105 - Support cc-type as valid field for credit cards in form autofill. r=MattN * Add cc-type as a valid field for credit card forms * Add a select menu and new string for designating a card type in the add/edit form * Enforce matching of cc-type to one of the list of supported network ids for Basic Card * Expose the network ids list as CreditCard.SUPPORTED_TYPES * Populate the cc-type options using a getCreditCardTypes util method passed into the EditCreditCard constructor * Web Payment tests: verify cc-type picker is presented, populated as expected and selections received in the response MozReview-Commit-ID: 9QyU1UwTRay Differential Revision: https://phabricator.services.mozilla.com/D3830
browser/components/payments/content/paymentDialogFrameScript.js
browser/components/payments/res/containers/basic-card-form.js
browser/components/payments/res/unprivileged-fallbacks.js
browser/components/payments/test/PaymentTestUtils.jsm
browser/components/payments/test/browser/browser_card_edit.js
browser/components/payments/test/browser/head.js
browser/components/payments/test/mochitest/test_basic_card_form.html
browser/extensions/formautofill/FormAutofillParent.jsm
browser/extensions/formautofill/FormAutofillStorage.jsm
browser/extensions/formautofill/FormAutofillUtils.jsm
browser/extensions/formautofill/content/autofillEditForms.js
browser/extensions/formautofill/content/editCreditCard.xhtml
browser/extensions/formautofill/content/l10n.js
browser/extensions/formautofill/content/manageDialog.js
browser/extensions/formautofill/locales/en-US/formautofill.properties
browser/extensions/formautofill/skin/shared/editCreditCard.css
browser/extensions/formautofill/test/browser/browser_creditCard_doorhanger.js
browser/extensions/formautofill/test/browser/browser_editCreditCardDialog.js
browser/extensions/formautofill/test/browser/head.js
browser/extensions/formautofill/test/fixtures/autocomplete_creditcard_basic.html
browser/extensions/formautofill/test/mochitest/test_basic_creditcard_autocomplete_form.html
browser/extensions/formautofill/test/unit/test_creditCardRecords.js
browser/extensions/formautofill/test/unit/test_getRecords.js
toolkit/modules/CreditCard.jsm
toolkit/modules/tests/xpcshell/test_CreditCard.js
--- a/browser/components/payments/content/paymentDialogFrameScript.js
+++ b/browser/components/payments/content/paymentDialogFrameScript.js
@@ -75,16 +75,21 @@ let PaymentFrameScript = {
     let PaymentDialogUtils = {
       DEFAULT_REGION: FormAutofill.DEFAULT_REGION,
       supportedCountries: FormAutofill.supportedCountries,
 
       getAddressLabel(address, addressFields = null) {
         return FormAutofillUtils.getAddressLabel(address, addressFields);
       },
 
+      getCreditCardNetworks() {
+        let networks = FormAutofillUtils.getCreditCardNetworks();
+        return Cu.cloneInto(networks, waivedContent);
+      },
+
       isCCNumber(value) {
         return FormAutofillUtils.isCCNumber(value);
       },
 
       getFormFormat(country) {
         let format = FormAutofillUtils.getFormFormat(country);
         return Cu.cloneInto(format, waivedContent);
       },
--- a/browser/components/payments/res/containers/basic-card-form.js
+++ b/browser/components/payments/res/containers/basic-card-form.js
@@ -79,16 +79,17 @@ export default class BasicCardForm exten
 
       let record = {};
       let addresses = [];
       this.formHandler = new EditCreditCard({
         form,
       }, record, addresses, {
         isCCNumber: PaymentDialogUtils.isCCNumber,
         getAddressLabel: PaymentDialogUtils.getAddressLabel,
+        getSupportedNetworks: PaymentDialogUtils.getCreditCardNetworks,
       });
 
       // The EditCreditCard constructor adds `input` event listeners on the same element,
       // which update field validity. By adding our event listeners after this constructor,
       // validity will be updated before our handlers get the event
       form.addEventListener("input", this);
       form.addEventListener("invalid", this);
 
--- a/browser/components/payments/res/unprivileged-fallbacks.js
+++ b/browser/components/payments/res/unprivileged-fallbacks.js
@@ -25,16 +25,32 @@ var PaymentDialogUtils = {
   getAddressLabel(address, addressFields = null) {
     if (addressFields) {
       let requestedFields = addressFields.trim().split(/\s+/);
       return requestedFields.filter(f => f && address[f]).map(f => address[f]).join(", ") +
         ` (${address.guid})`;
     }
     return `${address.name} (${address.guid})`;
   },
+
+  getCreditCardNetworks(address) {
+    // Shim for list of known and supported credit card network ids as exposed by
+    // toolkit/modules/CreditCard.jsm
+    return [
+      "amex",
+      "cartebancaire",
+      "diners",
+      "discover",
+      "jcb",
+      "mastercard",
+      "mir",
+      "unionpay",
+      "visa",
+    ];
+  },
   isCCNumber(str) {
     return !!str.replace(/[-\s]/g, "").match(/^\d{9,}$/);
   },
   DEFAULT_REGION: "US",
   supportedCountries: ["US", "CA", "DE"],
   getFormFormat(country) {
     if (country == "DE") {
       return {
--- a/browser/components/payments/test/PaymentTestUtils.jsm
+++ b/browser/components/payments/test/PaymentTestUtils.jsm
@@ -477,27 +477,30 @@ var PaymentTestUtils = {
   },
 
   BasicCards: {
     JohnDoe: {
       "cc-exp-month": 1,
       "cc-exp-year": (new Date()).getFullYear() + 9,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
+      "cc-type": "visa",
     },
     JaneMasterCard: {
       "cc-exp-month": 12,
       "cc-exp-year": (new Date()).getFullYear() + 9,
       "cc-name": "Jane McMaster-Card",
       "cc-number": "5555555555554444",
+      "cc-type": "mastercard",
     },
     MissingFields: {
       "cc-name": "Missy Fields",
       "cc-number": "340000000000009",
     },
     Temp: {
       "cc-exp-month": 12,
       "cc-exp-year": (new Date()).getFullYear() + 9,
       "cc-name": "Temp Name",
       "cc-number": "5105105105105100",
+      "cc-type": "mastercard",
     },
   },
 };
--- a/browser/components/payments/test/browser/browser_card_edit.js
+++ b/browser/components/payments/test/browser/browser_card_edit.js
@@ -45,22 +45,25 @@ async function add_link(aOptions = {}) {
 
       is(state.isPrivate, testArgs.isPrivate,
          "isPrivate flag has expected value when shown from a private/non-private session");
     }, aOptions);
 
     let cardOptions = Object.assign({}, {
       checkboxSelector: "basic-card-form .persist-checkbox",
       expectPersist: aOptions.expectCardPersist,
+      networkSelector: "basic-card-form #cc-type",
+      expectedNetwork: PTU.BasicCards.JaneMasterCard["cc-type"],
     });
     if (aOptions.hasOwnProperty("setCardPersistCheckedValue")) {
       cardOptions.setPersistCheckedValue = aOptions.setCardPersistCheckedValue;
     }
     await fillInCardForm(frame, PTU.BasicCards.JaneMasterCard, cardOptions);
 
+    await verifyCardNetwork(frame, cardOptions);
     await verifyPersistCheckbox(frame, cardOptions);
 
     await spawnPaymentDialogTask(frame, async function checkBillingAddressPicker(testArgs = {}) {
       let billingAddressSelect = content.document.querySelector("#billingAddressGUID");
       ok(content.isVisible(billingAddressSelect),
          "The billing address selector should always be visible");
       is(billingAddressSelect.childElementCount, 2,
          "Only 2 child options should exist by default");
@@ -514,16 +517,77 @@ add_task(async function test_edit_link()
     }
 
     state = await PTU.DialogContentUtils.waitForState(content, (state) => {
       return state.page.id == "payment-summary";
     }, "Switched back to payment-summary");
   }, args);
 });
 
+add_task(async function test_invalid_network_card_edit() {
+  // add an address and card linked to this address
+  let prefilledGuids = await setup([PTU.Addresses.TimBL]);
+  {
+    let card = Object.assign({}, PTU.BasicCards.JohnDoe,
+                             { billingAddressGUID: prefilledGuids.address1GUID });
+    // create a record with an unknown network id
+    card["cc-type"] = "asiv";
+    await addCardRecord(card);
+  }
+
+  const args = {
+    methodData: [PTU.MethodData.basicCard],
+    details: PTU.Details.total60USD,
+  };
+  await spawnInDialogForMerchantTask(PTU.ContentTasks.createAndShowRequest, async function check() {
+    let {
+      PaymentTestUtils: PTU,
+    } = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
+
+    let editLink = content.document.querySelector("payment-method-picker .edit-link");
+    is(editLink.textContent, "Edit", "Edit link text");
+
+    editLink.click();
+
+    let state = await PTU.DialogContentUtils.waitForState(content, (state) => {
+      return state.page.id == "basic-card-page" && state["basic-card-page"].guid;
+    }, "Check edit page state");
+
+    state = await PTU.DialogContentUtils.waitForState(content, (state) => {
+      return Object.keys(state.savedBasicCards).length == 1 &&
+             Object.keys(state.savedAddresses).length == 1;
+    }, "Check card and address present at beginning of test");
+
+    let networkSelector = content.document.querySelector("basic-card-form #cc-type");
+    todo_is(Cu.waiveXrays(networkSelector).selectedIndex, 0,
+            "An invalid cc-type should result in the first option being selected");
+    is(Cu.waiveXrays(networkSelector).value, "",
+       "An invalid cc-type should result in an empty string as value");
+
+    content.document.querySelector("basic-card-form button.save-button").click();
+
+    // we expect that saving a card with an invalid network will result in the
+    // cc-type property being changed to undefined
+    state = await PTU.DialogContentUtils.waitForState(content, (state) => {
+      let cards = Object.entries(state.savedBasicCards);
+      return cards.length == 1 &&
+             cards[0][1]["cc-type"] == undefined;
+    }, "Check card was edited");
+
+    let cardGUIDs = Object.keys(state.savedBasicCards);
+    is(cardGUIDs.length, 1, "Check there is still one card");
+    let savedCard = state.savedBasicCards[cardGUIDs[0]];
+    ok(!savedCard["cc-type"], "We expect the cc-type value to be updated");
+
+    state = await PTU.DialogContentUtils.waitForState(content, (state) => {
+      return state.page.id == "payment-summary";
+    }, "Switched back to payment-summary");
+  }, args);
+});
+
 add_task(async function test_private_card_adding() {
   await setup([PTU.Addresses.TimBL], [PTU.BasicCards.JohnDoe]);
   let privateWin = await BrowserTestUtils.openNewBrowserWindow({private: true});
 
   await BrowserTestUtils.withNewTab({
     gBrowser: privateWin.gBrowser,
     url: BLANK_PAGE_URL,
   }, async browser => {
@@ -586,8 +650,9 @@ add_task(async function test_private_car
       ok(tempCard["cc-exp"], "cc-exp was computed");
       ok(tempCard["cc-number-encrypted"], "cc-number-encrypted was computed");
     });
     spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
     await BrowserTestUtils.waitForCondition(() => win.closed, "dialog should be closed");
   });
   await BrowserTestUtils.closeWindow(privateWin);
 });
+
--- a/browser/components/payments/test/browser/head.js
+++ b/browser/components/payments/test/browser/head.js
@@ -17,16 +17,17 @@ const SAVE_ADDRESS_DEFAULT_PREF = "dom.p
 const paymentSrv = Cc["@mozilla.org/dom/payments/payment-request-service;1"]
                      .getService(Ci.nsIPaymentRequestService);
 const paymentUISrv = Cc["@mozilla.org/dom/payments/payment-ui-service;1"]
                      .getService().wrappedJSObject;
 const {formAutofillStorage} = ChromeUtils.import(
   "resource://formautofill/FormAutofillStorage.jsm", {});
 const {PaymentTestUtils: PTU} = ChromeUtils.import(
   "resource://testing-common/PaymentTestUtils.jsm", {});
+ChromeUtils.import("resource://gre/modules/CreditCard.jsm");
 
 function getPaymentRequests() {
   return Array.from(paymentSrv.enumerate());
 }
 
 /**
  * Return the container (e.g. dialog or overlay) that the payment request contents are shown in.
  * This abstracts away the details of the widget used so that this can more earily transition from a
@@ -352,16 +353,19 @@ add_task(async function setup_head() {
   await setupFormAutofillStorage();
   registerCleanupFunction(function cleanup() {
     paymentSrv.cleanup();
     cleanupFormAutofillStorage();
     Services.prefs.clearUserPref(RESPONSE_TIMEOUT_PREF);
     Services.prefs.clearUserPref(SAVE_CREDITCARD_DEFAULT_PREF);
     Services.prefs.clearUserPref(SAVE_ADDRESS_DEFAULT_PREF);
     SpecialPowers.postConsoleSentinel();
+    // CreditCard.jsm is imported into the global scope. It needs to be deleted
+    // else it outlives the test and is reported as a leak.
+    delete window.CreditCard;
   });
 });
 
 function deepClone(obj) {
   return JSON.parse(JSON.stringify(obj));
 }
 
 async function selectPaymentDialogShippingAddressByCountry(frame, country) {
@@ -470,16 +474,35 @@ async function verifyPersistCheckbox(fra
     } else {
       ok(!persistCheckbox.hidden, "checkbox should be visible when adding a new record");
       is(persistCheckbox.checked, options.expectPersist,
          `persist checkbox state is expected to be ${options.expectPersist}`);
     }
   }, {options: aOptions});
 }
 
+async function verifyCardNetwork(frame, aOptions = {}) {
+  aOptions.supportedNetworks = CreditCard.SUPPORTED_NETWORKS;
+
+  await spawnPaymentDialogTask(frame, async (args) => {
+    let {options = {}} = args;
+    // ensure the network picker is visible, has the right contents and expected value
+    let networkSelect = Cu.waiveXrays(
+        content.document.querySelector(options.networkSelector));
+    ok(content.isVisible(networkSelect),
+       "The network selector should always be visible");
+    is(networkSelect.childElementCount, options.supportedNetworks.length + 1,
+       "Should have one more than the number of supported networks");
+    is(networkSelect.children[0].value, "",
+       "The first option should be the blank/empty option");
+    is(networkSelect.value, options.expectedNetwork,
+       `The network picker should have the expected value`);
+  }, {options: aOptions});
+}
+
 async function submitAddressForm(frame, aAddress, aOptions = {}) {
   await spawnPaymentDialogTask(frame, async (args) => {
     let {options = {}} = args;
     let {
       PaymentTestUtils,
     } = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
 
     let oldAddresses = await PaymentTestUtils.DialogContentUtils.getCurrentState(content);
--- a/browser/components/payments/test/mochitest/test_basic_card_form.html
+++ b/browser/components/payments/test/mochitest/test_basic_card_form.html
@@ -37,16 +37,17 @@ let display = document.getElementById("d
 
 function checkCCForm(customEl, expectedCard) {
   const CC_PROPERTY_NAMES = [
     "billingAddressGUID",
     "cc-number",
     "cc-name",
     "cc-exp-month",
     "cc-exp-year",
+    "cc-type",
   ];
   for (let propName of CC_PROPERTY_NAMES) {
     let expectedVal = expectedCard[propName] || "";
     is(document.getElementById(propName).value,
        expectedVal.toString(),
        `Check ${propName}`);
   }
 }
@@ -105,16 +106,17 @@ add_task(async function test_saveButton(
   fillField(form.form.querySelector("#cc-number"), "4111 1111-1111 1111");
   form.form.querySelector("#cc-name").focus();
   // Check .disabled after .focus() so that it's after both "input" and "change" events.
   ok(form.saveButton.disabled, "Save button should still be disabled without a name");
   sendString("J. Smith");
   fillField(form.form.querySelector("#cc-exp-month"), "11");
   let year = (new Date()).getFullYear().toString();
   fillField(form.form.querySelector("#cc-exp-year"), year);
+  fillField(form.form.querySelector("#cc-type"), "visa");
   form.saveButton.focus();
   ok(!form.saveButton.disabled,
      "Save button should be enabled since the required fields are filled");
 
   info("blanking the cc-number field");
   fillField(form.form.querySelector("#cc-number"), "");
   ok(form.saveButton.disabled, "Save button is disabled after blanking cc-number");
   form.form.querySelector("#cc-number").blur();
@@ -138,16 +140,17 @@ add_task(async function test_saveButton(
     collectionName: "creditCards",
     guid: undefined,
     messageType: "updateAutofillRecord",
     record: {
       "cc-exp-month": "11",
       "cc-exp-year": year,
       "cc-name": "J. Smith",
       "cc-number": "4111 1111-1111 1111",
+      "cc-type": "visa",
       "billingAddressGUID": "",
       "isTemporary": true,
     },
   }, "Check event details for the message to chrome");
   form.remove();
 });
 
 add_task(async function test_requiredAttributePropagated() {
@@ -434,12 +437,43 @@ add_task(async function test_numberCusto
     "basic-card-page": {
     },
   });
 
   ok(!form.querySelector("#cc-number:-moz-ui-invalid"), "cc-number field is not visibly invalid");
 
   form.remove();
 });
+
+add_task(async function test_noCardNetworkSelected() {
+  let form = new BasicCardForm();
+  await form.promiseReady;
+  display.appendChild(form);
+  await asyncElementRendered();
+
+  info("have an existing card in storage, with no network id");
+  let card1 = deepClone(PTU.BasicCards.JohnDoe);
+  card1.guid = "9864798564";
+  delete card1["cc-type"];
+
+  await form.requestStore.setState({
+    page: {
+      id: "basic-card-page",
+    },
+    "basic-card-page": {
+      guid: card1.guid,
+    },
+    savedBasicCards: {
+      [card1.guid]: deepClone(card1),
+    },
+  });
+  await asyncElementRendered();
+  checkCCForm(form, card1);
+  is(document.getElementById("cc-type").selectedIndex, 0, "Initial empty option is selected");
+
+  form.remove();
+  await form.requestStore.reset();
+});
+
 </script>
 
 </body>
 </html>
--- a/browser/extensions/formautofill/FormAutofillParent.jsm
+++ b/browser/extensions/formautofill/FormAutofillParent.jsm
@@ -459,16 +459,21 @@ FormAutofillParent.prototype = {
     // Updates the used status for shield/heartbeat to recognize users who have
     // used Credit Card Autofill.
     let setUsedStatus = status => {
       if (FormAutofill.AutofillCreditCardsUsedStatus < status) {
         Services.prefs.setIntPref(FormAutofill.CREDITCARDS_USED_STATUS_PREF, status);
       }
     };
 
+    // Remove invalid cc-type values
+    if (creditCard.record["cc-type"] && !CreditCard.isValidNetwork(creditCard.record["cc-type"])) {
+      delete creditCard.record["cc-type"];
+    }
+
     // We'll show the credit card doorhanger if:
     //   - User applys autofill and changed
     //   - User fills form manually and the filling data is not duplicated to storage
     if (creditCard.guid) {
       // Indicate that the user has used Credit Card Autofill to fill in a form.
       setUsedStatus(3);
 
       let originalCCData = this.formAutofillStorage.creditCards.get(creditCard.guid);
@@ -523,16 +528,17 @@ FormAutofillParent.prototype = {
       if (!FormAutofill.isAutofillCreditCardsEnabled) {
         return;
       }
 
       const card = new CreditCard({
         number: creditCard.record["cc-number"] || creditCard.record["cc-number-decrypted"],
         encryptedNumber: creditCard.record["cc-number-encrypted"],
         name: creditCard.record["cc-name"],
+        network: creditCard.record["cc-type"],
       });
       const description = await card.getLabel();
       const state = await FormAutofillDoorhanger.show(target,
                                                       creditCard.guid ? "updateCreditCard" : "addCreditCard",
                                                       description);
       if (state == "cancel") {
         return;
       }
--- a/browser/extensions/formautofill/FormAutofillStorage.jsm
+++ b/browser/extensions/formautofill/FormAutofillStorage.jsm
@@ -61,16 +61,17 @@
  *                                which may or may not exist locally.
  *
  *       cc-name,
  *       cc-number,            // will be stored in masked format (************1234)
  *                             // (see details below)
  *       cc-exp-month,
  *       cc-exp-year,          // 2-digit year will be converted to 4 digits
  *                             // upon saving
+ *       cc-type,              // Optional card network id (instrument type)
  *
  *       // computed fields (These fields are computed based on the above fields
  *       // and are not allowed to be modified directly.)
  *       cc-given-name,
  *       cc-additional-name,
  *       cc-family-name,
  *       cc-number-encrypted,  // encrypted from the original unmasked "cc-number"
  *                             // (see details below)
@@ -193,16 +194,17 @@ const VALID_ADDRESS_COMPUTED_FIELDS = [
 ].concat(STREET_ADDRESS_COMPONENTS, TEL_COMPONENTS);
 
 const VALID_CREDIT_CARD_FIELDS = [
   "billingAddressGUID",
   "cc-name",
   "cc-number",
   "cc-exp-month",
   "cc-exp-year",
+  "cc-type",
 ];
 
 const VALID_CREDIT_CARD_COMPUTED_FIELDS = [
   "cc-given-name",
   "cc-additional-name",
   "cc-family-name",
   "cc-number-encrypted",
   "cc-exp",
--- a/browser/extensions/formautofill/FormAutofillUtils.jsm
+++ b/browser/extensions/formautofill/FormAutofillUtils.jsm
@@ -13,17 +13,17 @@ const ADDRESS_REFERENCES_EXT = "addressR
 const ADDRESSES_COLLECTION_NAME = "addresses";
 const CREDITCARDS_COLLECTION_NAME = "creditCards";
 const MANAGE_ADDRESSES_KEYWORDS = ["manageAddressesTitle", "addNewAddressTitle"];
 const EDIT_ADDRESS_KEYWORDS = [
   "givenName", "additionalName", "familyName", "organization2", "streetAddress",
   "state", "province", "city", "country", "zip", "postalCode", "email", "tel",
 ];
 const MANAGE_CREDITCARDS_KEYWORDS = ["manageCreditCardsTitle", "addNewCreditCardTitle", "showCreditCardsBtnLabel"];
-const EDIT_CREDITCARD_KEYWORDS = ["cardNumber", "nameOnCard", "cardExpiresMonth", "cardExpiresYear"];
+const EDIT_CREDITCARD_KEYWORDS = ["cardNumber", "nameOnCard", "cardExpiresMonth", "cardExpiresYear", "cardNetwork"];
 const FIELD_STATES = {
   NORMAL: "NORMAL",
   AUTO_FILLED: "AUTO_FILLED",
   PREVIEW: "PREVIEW",
 };
 const SECTION_TYPES = {
   ADDRESS: "address",
   CREDIT_CARD: "creditCard",
@@ -201,16 +201,17 @@ this.FormAutofillUtils = {
     "cc-name": "creditCard",
     "cc-given-name": "creditCard",
     "cc-additional-name": "creditCard",
     "cc-family-name": "creditCard",
     "cc-number": "creditCard",
     "cc-exp-month": "creditCard",
     "cc-exp-year": "creditCard",
     "cc-exp": "creditCard",
+    "cc-type": "creditCard",
   },
 
   _collators: {},
   _reAlternativeCountryNames: {},
 
   isAddressField(fieldName) {
     return !!this._fieldNameInfo[fieldName] && !this.isCreditCardField(fieldName);
   },
@@ -219,16 +220,25 @@ this.FormAutofillUtils = {
     return this._fieldNameInfo[fieldName] == "creditCard";
   },
 
   isCCNumber(ccNumber) {
     let card = new CreditCard({number: ccNumber});
     return card.isValidNumber();
   },
 
+  /**
+   * Get the array of credit card network ids ("types") we expect and offer as valid choices
+   *
+   * @returns {Array}
+   */
+  getCreditCardNetworks() {
+    return CreditCard.SUPPORTED_NETWORKS;
+  },
+
   getCategoryFromFieldName(fieldName) {
     return this._fieldNameInfo[fieldName];
   },
 
   getCategoriesFromFieldNames(fieldNames) {
     let categories = new Set();
     for (let fieldName of fieldNames) {
       let info = this.getCategoryFromFieldName(fieldName);
@@ -697,16 +707,27 @@ this.FormAutofillUtils = {
           if ([option.text, option.label, option.value].some(
             str => patterns.some(pattern => str.includes(pattern))
           )) {
             return option;
           }
         }
         break;
       }
+      case "cc-type": {
+        let network = creditCard["cc-type"] || "";
+        for (let option of options) {
+          if ([option.text, option.label, option.value].some(
+            s => s.trim().toLowerCase() == network
+          )) {
+            return option;
+          }
+        }
+        break;
+      }
     }
 
     return null;
   },
 
   /**
    * Try to match value with keys and names, but always return the key.
    * @param   {array<string>} keys
--- a/browser/extensions/formautofill/content/autofillEditForms.js
+++ b/browser/extensions/formautofill/content/autofillEditForms.js
@@ -296,41 +296,45 @@ class EditAddress extends EditAutofillFo
 
 class EditCreditCard extends EditAutofillForm {
   /**
    * @param {HTMLElement[]} elements
    * @param {object} record with a decrypted cc-number
    * @param {object} addresses in an object with guid keys for the billing address picker.
    * @param {object} config
    * @param {function} config.isCCNumber Function to determine if a string is a valid CC number.
+   * @param {function} config.getSupportedNetworks Function to get the list of card networks
    */
   constructor(elements, record, addresses, config) {
     super(elements);
 
     this._addresses = addresses;
     Object.assign(this, config);
     Object.assign(this._elements, {
       ccNumber: this._elements.form.querySelector("#cc-number"),
       invalidCardNumberStringElement: this._elements.form.querySelector("#invalidCardNumberString"),
       month: this._elements.form.querySelector("#cc-exp-month"),
       year: this._elements.form.querySelector("#cc-exp-year"),
+      ccType: this._elements.form.querySelector("#cc-type"),
       billingAddress: this._elements.form.querySelector("#billingAddressGUID"),
       billingAddressRow: this._elements.form.querySelector(".billingAddressRow"),
     });
 
     this.loadRecord(record, addresses);
     this.attachEventListeners();
   }
 
   loadRecord(record, addresses, preserveFieldValues) {
     // _record must be updated before generateYears and generateBillingAddressOptions are called.
     this._record = record;
     this._addresses = addresses;
     this.generateBillingAddressOptions();
     if (!preserveFieldValues) {
+      // Re-populating the networks will reset the selected option.
+      this.populateNetworks();
       // Re-generating the years will reset the selected option.
       this.generateYears();
       super.loadRecord(record);
 
       // Resetting the form in the super.loadRecord won't clear custom validity
       // state so reset it here. Since the cc-number field is disabled upon editing
       // we don't need to recaclulate its validity here.
       this._elements.ccNumber.setCustomValidity("");
@@ -358,16 +362,33 @@ class EditCreditCard extends EditAutofil
       this._elements.year.appendChild(option);
     }
 
     if (ccExpYear && ccExpYear > currentYear + count) {
       this._elements.year.appendChild(new Option(ccExpYear));
     }
   }
 
+  populateNetworks() {
+    // Clear the list
+    this._elements.ccType.textContent = "";
+    let frag = document.createDocumentFragment();
+    // include an empty first option
+    frag.appendChild(new Option("", ""));
+
+    let supportedNetworks = this.getSupportedNetworks();
+    for (let id of supportedNetworks) {
+      let option = new Option();
+      option.value = id;
+      option.dataset.localization = "cardNetwork." + id;
+      frag.appendChild(option);
+    }
+    this._elements.ccType.appendChild(frag);
+  }
+
   generateBillingAddressOptions() {
     let billingAddressGUID = this._record && this._record.billingAddressGUID;
 
     this._elements.billingAddress.textContent = "";
 
     this._elements.billingAddress.appendChild(new Option("", ""));
 
     let hasAddresses = false;
--- a/browser/extensions/formautofill/content/editCreditCard.xhtml
+++ b/browser/extensions/formautofill/content/editCreditCard.xhtml
@@ -51,19 +51,19 @@
       </select>
       <span data-localization="cardExpiresYear" class="label-text"/>
     </label>
     <label id="cc-name-container" class="container">
       <input id="cc-name" type="text" required="required"/>
       <span data-localization="nameOnCard" class="label-text"/>
     </label>
     <label id="cc-type-container" class="container">
-      <select id="cc-type" disabled="disabled">
+      <select id="cc-type">
       </select>
-      <span class="label-text">Card Type</span>
+      <span data-localization="cardNetwork" class="label-text"/>
     </label>
     <label id="billingAddressGUID-container" class="billingAddressRow container">
       <select id="billingAddressGUID">
       </select>
       <span data-localization="billingAddress" class="label-text"/>
     </label>
   </form>
   <div id="controls-container">
@@ -71,30 +71,32 @@
     <button id="save" disabled="disabled" data-localization="saveBtnLabel"/>
   </div>
   <script type="application/javascript"><![CDATA[
     "use strict";
 
     let {
       getAddressLabel,
       isCCNumber,
+      getCreditCardNetworks,
     } = FormAutofillUtils;
     let record = window.arguments && window.arguments[0];
     let addresses = {};
     for (let address of formAutofillStorage.addresses.getAll()) {
       addresses[address.guid] = address;
     }
 
     /* import-globals-from autofillEditForms.js */
     let fieldContainer = new EditCreditCard({
       form: document.getElementById("form"),
     }, record, addresses,
       {
         getAddressLabel: getAddressLabel.bind(FormAutofillUtils),
         isCCNumber: isCCNumber.bind(FormAutofillUtils),
+        getSupportedNetworks: getCreditCardNetworks.bind(FormAutofillUtils),
       });
 
     /* import-globals-from editDialog.js */
     new EditCreditCardDialog({
       title: document.querySelector("title"),
       fieldContainer,
       controlsContainer: document.getElementById("controls-container"),
       cancel: document.getElementById("cancel"),
--- a/browser/extensions/formautofill/content/l10n.js
+++ b/browser/extensions/formautofill/content/l10n.js
@@ -31,19 +31,18 @@ CONTENT_WIN.addEventListener("DOMContent
             // The attribute was removed in the meantime.
             continue;
           }
           FormAutofillUtils.localizeAttributeForElement(mutation.target, mutation.attributeName);
           break;
         }
 
         case "childList": {
-          // We really only care about the <form>s appending inside pages.
-          if (!mutation.addedNodes || !mutation.target.classList ||
-              !mutation.target.classList.contains("page")) {
+          // We really only care about elements appending inside pages.
+          if (!mutation.addedNodes || !mutation.target.closest(".page")) {
             break;
           }
           FormAutofillUtils.localizeMarkup(mutation.target);
           break;
         }
       }
     }
   });
--- a/browser/extensions/formautofill/content/manageDialog.js
+++ b/browser/extensions/formautofill/content/manageDialog.js
@@ -341,16 +341,17 @@ class ManageCreditCards extends ManageRe
    * @param {boolean} showCreditCards [optional]
    * @returns {string}
    */
   async getLabel(creditCard, showCreditCards = false) {
     let cardObj = new CreditCard({
       encryptedNumber: creditCard["cc-number-encrypted"],
       number: creditCard["cc-number"],
       name: creditCard["cc-name"],
+      network: creditCard["cc-type"],
     });
     return cardObj.getLabel({showNumbers: showCreditCards});
   }
 
   async toggleShowHideCards(options) {
     this._isDecrypted = !this._isDecrypted;
     this.updateShowHideButtonState();
     await this.updateLabels(options, this._isDecrypted);
--- a/browser/extensions/formautofill/locales/en-US/formautofill.properties
+++ b/browser/extensions/formautofill/locales/en-US/formautofill.properties
@@ -136,8 +136,20 @@ countryWarningMessage2 = Form Autofill i
 addNewCreditCardTitle = Add New Credit Card
 editCreditCardTitle = Edit Credit Card
 cardNumber = Card Number
 invalidCardNumber = Please enter a valid card number
 nameOnCard = Name on Card
 cardExpiresMonth = Exp. Month
 cardExpiresYear = Exp. Year
 billingAddress = Billing Address
+cardNetwork = Card Type
+
+# LOCALIZATION NOTE: (cardNetwork.*): These are brand names and should only be translated when a locale-specific name for that brand is in common use
+cardNetwork.amex = American Express
+cardNetwork.cartebancaire = Carte Bancaire
+cardNetwork.diners = Diners Club
+cardNetwork.discover = Discover
+cardNetwork.jcb = JCB
+cardNetwork.mastercard = MasterCard
+cardNetwork.mir = MIR
+cardNetwork.unionpay = Union Pay
+cardNetwork.visa = Visa
--- a/browser/extensions/formautofill/skin/shared/editCreditCard.css
+++ b/browser/extensions/formautofill/skin/shared/editCreditCard.css
@@ -36,14 +36,13 @@
 }
 
 #cc-name-container {
   grid-area: cc-name;
 }
 
 #cc-type-container {
   grid-area: cc-type;
-  visibility: hidden; /* TODO: Bug 1477105 */
 }
 
 #billingAddressGUID-container {
   grid-area: billingAddressGUID;
 }
--- a/browser/extensions/formautofill/test/browser/browser_creditCard_doorhanger.js
+++ b/browser/extensions/formautofill/test/browser/browser_creditCard_doorhanger.js
@@ -55,30 +55,32 @@ add_task(async function test_submit_cred
         let form = content.document.getElementById("form");
         let name = form.querySelector("#cc-name");
         name.focus();
         name.setUserInput("User 1");
 
         form.querySelector("#cc-number").setUserInput("5038146897157463");
         form.querySelector("#cc-exp-month").setUserInput("12");
         form.querySelector("#cc-exp-year").setUserInput("2017");
+        form.querySelector("#cc-type").value = "mastercard";
 
         // Wait 1000ms before submission to make sure the input value applied
         await new Promise(resolve => setTimeout(resolve, 1000));
         form.querySelector("input[type=submit]").click();
       });
 
       await promiseShown;
       await clickDoorhangerButton(MAIN_BUTTON);
     }
   );
 
   let creditCards = await getCreditCards();
   is(creditCards.length, 1, "1 credit card in storage");
   is(creditCards[0]["cc-name"], "User 1", "Verify the name field");
+  is(creditCards[0]["cc-type"], "mastercard", "Verify the cc-type field");
   is(SpecialPowers.getIntPref(CREDITCARDS_USED_STATUS_PREF), 2, "User has seen the doorhanger");
   SpecialPowers.clearUserPref(CREDITCARDS_USED_STATUS_PREF);
   await removeAllRecords();
 });
 
 add_task(async function test_submit_untouched_creditCard_form() {
   await SpecialPowers.pushPrefEnv({
     "set": [
@@ -171,16 +173,17 @@ add_task(async function test_submit_dupl
         let form = content.document.getElementById("form");
         let name = form.querySelector("#cc-name");
         name.focus();
 
         name.setUserInput("John Doe");
         form.querySelector("#cc-number").setUserInput("4111111111111111");
         form.querySelector("#cc-exp-month").setUserInput("4");
         form.querySelector("#cc-exp-year").setUserInput(new Date().getFullYear());
+        form.querySelector("#cc-type").value = "visa";
 
         // Wait 1000ms before submission to make sure the input value applied
         await new Promise(resolve => setTimeout(resolve, 1000));
         form.querySelector("input[type=submit]").click();
       });
 
       await sleep(1000);
       is(PopupNotifications.panel.state, "closed", "Doorhanger is hidden");
@@ -208,16 +211,17 @@ add_task(async function test_submit_unno
         let name = form.querySelector("#cc-name");
         name.focus();
 
         name.setUserInput("John Doe");
         form.querySelector("#cc-number").setUserInput("4111111111111111");
         form.querySelector("#cc-exp-month").setUserInput("4");
         // Set unnormalized year
         form.querySelector("#cc-exp-year").setUserInput(new Date().getFullYear().toString().substr(2, 2));
+        form.querySelector("#cc-type").value = "visa";
 
         // Wait 1000ms before submission to make sure the input value applied
         await new Promise(resolve => setTimeout(resolve, 1000));
         form.querySelector("input[type=submit]").click();
       });
 
       await sleep(1000);
       is(PopupNotifications.panel.state, "closed", "Doorhanger is hidden");
@@ -631,8 +635,44 @@ add_task(async function test_update_dupl
 
   creditCards = await getCreditCards();
   is(creditCards.length, 2, "Still 2 credit card");
   is(SpecialPowers.getIntPref(CREDITCARDS_USED_STATUS_PREF), 1,
     "User neither sees the doorhanger nor uses autofill but somehow has a record in the storage");
   SpecialPowers.clearUserPref(CREDITCARDS_USED_STATUS_PREF);
   await removeAllRecords();
 });
+
+add_task(async function test_submit_creditCard_with_invalid_network() {
+  await BrowserTestUtils.withNewTab({gBrowser, url: CREDITCARD_FORM_URL},
+    async function(browser) {
+      let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
+                                                       "popupshown");
+      await ContentTask.spawn(browser, null, async function() {
+        let form = content.document.getElementById("form");
+        let name = form.querySelector("#cc-name");
+        name.focus();
+        name.setUserInput("User 1");
+
+        form.querySelector("#cc-number").setUserInput("5038146897157463");
+        form.querySelector("#cc-exp-month").setUserInput("12");
+        form.querySelector("#cc-exp-year").setUserInput("2017");
+        form.querySelector("#cc-type").value = "gringotts";
+
+        // Wait 1000ms before submission to make sure the input value applied
+        await new Promise(resolve => setTimeout(resolve, 1000));
+        form.querySelector("input[type=submit]").click();
+      });
+
+      await promiseShown;
+      await clickDoorhangerButton(MAIN_BUTTON);
+    }
+  );
+
+  let creditCards = await getCreditCards();
+  is(creditCards.length, 1, "1 credit card in storage");
+  is(creditCards[0]["cc-name"], "User 1", "Verify the name field");
+  is(creditCards[0]["cc-type"], undefined, "Invalid network/cc-type was not saved");
+
+  SpecialPowers.clearUserPref(CREDITCARDS_USED_STATUS_PREF);
+  await removeAllRecords();
+});
+
--- a/browser/extensions/formautofill/test/browser/browser_editCreditCardDialog.js
+++ b/browser/extensions/formautofill/test/browser/browser_editCreditCardDialog.js
@@ -25,16 +25,18 @@ add_task(async function test_saveCreditC
     EventUtils.synthesizeKey(TEST_CREDIT_CARD_1["cc-number"], {}, win);
     EventUtils.synthesizeKey("VK_TAB", {}, win);
     EventUtils.synthesizeKey("0" + TEST_CREDIT_CARD_1["cc-exp-month"].toString(), {}, win);
     EventUtils.synthesizeKey("VK_TAB", {}, win);
     EventUtils.synthesizeKey(TEST_CREDIT_CARD_1["cc-exp-year"].toString(), {}, win);
     EventUtils.synthesizeKey("VK_TAB", {}, win);
     EventUtils.synthesizeKey(TEST_CREDIT_CARD_1["cc-name"], {}, win);
     EventUtils.synthesizeKey("VK_TAB", {}, win);
+    EventUtils.synthesizeKey(TEST_CREDIT_CARD_1["cc-type"], {}, win);
+    EventUtils.synthesizeKey("VK_TAB", {}, win);
     EventUtils.synthesizeKey("VK_TAB", {}, win);
     EventUtils.synthesizeKey("VK_TAB", {}, win);
     info("saving credit card");
     EventUtils.synthesizeKey("VK_RETURN", {}, win);
   });
   let creditCards = await getCreditCards();
 
   is(creditCards.length, 1, "only one credit card is in storage");
@@ -54,16 +56,18 @@ add_task(async function test_saveCreditC
     EventUtils.synthesizeKey(TEST_CREDIT_CARD_2["cc-number"], {}, win);
     EventUtils.synthesizeKey("VK_TAB", {}, win);
     EventUtils.synthesizeKey(TEST_CREDIT_CARD_2["cc-exp-month"].toString(), {}, win);
     EventUtils.synthesizeKey("VK_TAB", {}, win);
     EventUtils.synthesizeKey(TEST_CREDIT_CARD_2["cc-exp-year"].toString(), {}, win);
     EventUtils.synthesizeKey("VK_TAB", {}, win);
     EventUtils.synthesizeKey(TEST_CREDIT_CARD_2["cc-name"], {}, win);
     EventUtils.synthesizeKey("VK_TAB", {}, win);
+    EventUtils.synthesizeKey(TEST_CREDIT_CARD_1["cc-type"], {}, win);
+    EventUtils.synthesizeKey("VK_TAB", {}, win);
     EventUtils.synthesizeKey("VK_TAB", {}, win);
     EventUtils.synthesizeKey("VK_TAB", {}, win);
     info("saving credit card");
     EventUtils.synthesizeKey("VK_RETURN", {}, win);
   });
   let creditCards = await getCreditCards();
 
   is(creditCards.length, 2, "Two credit cards are in storage");
@@ -92,16 +96,18 @@ add_task(async function test_saveCreditC
     EventUtils.synthesizeKey(TEST_CREDIT_CARD["cc-number"], {}, win);
     EventUtils.synthesizeKey("VK_TAB", {}, win);
     EventUtils.synthesizeKey(TEST_CREDIT_CARD["cc-exp-month"].toString(), {}, win);
     EventUtils.synthesizeKey("VK_TAB", {}, win);
     EventUtils.synthesizeKey(TEST_CREDIT_CARD["cc-exp-year"].toString(), {}, win);
     EventUtils.synthesizeKey("VK_TAB", {}, win);
     EventUtils.synthesizeKey(TEST_CREDIT_CARD["cc-name"], {}, win);
     EventUtils.synthesizeKey("VK_TAB", {}, win);
+    EventUtils.synthesizeKey(TEST_CREDIT_CARD["cc-type"], {}, win);
+    EventUtils.synthesizeKey("VK_TAB", {}, win);
     EventUtils.synthesizeKey(billingAddress["given-name"], {}, win);
     EventUtils.synthesizeKey("VK_TAB", {}, win);
     EventUtils.synthesizeKey("VK_TAB", {}, win);
     info("saving credit card");
     EventUtils.synthesizeKey("VK_RETURN", {}, win);
   });
   let creditCards = await getCreditCards();
 
@@ -198,8 +204,40 @@ add_task(async function test_addInvalidC
       win.close();
     }, 500);
   });
   info("closed");
   let creditCards = await getCreditCards();
 
   is(creditCards.length, 0, "Credit card storage is empty");
 });
+
+add_task(async function test_editCardWithInvalidNetwork() {
+  const TEST_CREDIT_CARD = Object.assign({}, TEST_CREDIT_CARD_2, {
+    "cc-type": "asiv",
+  });
+  await saveCreditCard(TEST_CREDIT_CARD);
+
+  let creditCards = await getCreditCards();
+  is(creditCards.length, 1, "one credit card in storage");
+  is(creditCards[0]["cc-type"], TEST_CREDIT_CARD["cc-type"],
+     "Check saved cc-type");
+  await testDialog(EDIT_CREDIT_CARD_DIALOG_URL, (win) => {
+    EventUtils.synthesizeKey("VK_TAB", {}, win);
+    EventUtils.synthesizeKey("VK_TAB", {}, win);
+    EventUtils.synthesizeKey("VK_TAB", {}, win);
+    EventUtils.synthesizeKey("VK_TAB", {}, win);
+    EventUtils.synthesizeKey("VK_RIGHT", {}, win);
+    EventUtils.synthesizeKey("test", {}, win);
+    win.document.querySelector("#save").click();
+  }, creditCards[0]);
+  ok(true, "Edit credit card dialog is closed");
+  creditCards = await getCreditCards();
+
+  is(creditCards.length, 1, "only one credit card is in storage");
+  is(creditCards[0]["cc-name"], TEST_CREDIT_CARD["cc-name"] + "test", "cc name changed");
+  is(creditCards[0]["cc-type"], undefined,
+     "unknown cc-type removed upon manual save");
+  await removeCreditCards([creditCards[0].guid]);
+
+  creditCards = await getCreditCards();
+  is(creditCards.length, 0, "Credit card storage is empty");
+});
--- a/browser/extensions/formautofill/test/browser/head.js
+++ b/browser/extensions/formautofill/test/browser/head.js
@@ -99,29 +99,32 @@ const TEST_ADDRESS_DE_1 = {
   email: "timbl@w3.org",
 };
 
 const TEST_CREDIT_CARD_1 = {
   "cc-name": "John Doe",
   "cc-number": "4111111111111111",
   "cc-exp-month": 4,
   "cc-exp-year": new Date().getFullYear(),
+  "cc-type": "visa",
 };
 
 const TEST_CREDIT_CARD_2 = {
   "cc-name": "Timothy Berners-Lee",
   "cc-number": "4929001587121045",
   "cc-exp-month": 12,
   "cc-exp-year": new Date().getFullYear() + 10,
+  "cc-type": "visa",
 };
 
 const TEST_CREDIT_CARD_3 = {
   "cc-number": "5103059495477870",
   "cc-exp-month": 1,
   "cc-exp-year": 2000,
+  "cc-type": "mastercard",
 };
 
 const MAIN_BUTTON = "button";
 const SECONDARY_BUTTON = "secondaryButton";
 const MENU_BUTTON = "menubutton";
 
 function getDisplayedPopupItems(browser, selector = ".autocomplete-richlistitem") {
   info("getDisplayedPopupItems");
--- a/browser/extensions/formautofill/test/fixtures/autocomplete_creditcard_basic.html
+++ b/browser/extensions/formautofill/test/fixtures/autocomplete_creditcard_basic.html
@@ -7,15 +7,23 @@
 <body>
   <h1>Form Autofill Credit Card Demo Page</h1>
   <form id="form">
     <p><label>Name: <input id="cc-name" autocomplete="cc-name"></label></p>
     <p><label>Card Number: <input id="cc-number" autocomplete="cc-number"></label></p>
     <p><label>Expiration month: <input id="cc-exp-month" autocomplete="cc-exp-month"></label></p>
     <p><label>Expiration year: <input id="cc-exp-year" autocomplete="cc-exp-year"></label></p>
     <p><label>CSC: <input id="cc-csc" autocomplete="cc-csc"></label></p>
+    <p><label>Card Type: <select id="cc-type" autocomplete="cc-type">
+      <option></option>
+      <option value="discover">Discover</option>
+      <option value="jcb">JCB</option>
+      <option value="visa">Visa</option>
+      <option value="mastercard">MasterCard</option>
+      <option value="gringotts">Unknown card network</option>
+    </select></label></p>
     <p>
       <input type="submit" value="Submit">
       <button type="reset">Reset</button>
     </p>
   </form>
 </body>
 </html>
--- a/browser/extensions/formautofill/test/mochitest/test_basic_creditcard_autocomplete_form.html
+++ b/browser/extensions/formautofill/test/mochitest/test_basic_creditcard_autocomplete_form.html
@@ -18,21 +18,23 @@ Form autofill test: simple form credit c
 
 "use strict";
 
 const MOCK_STORAGE = [{
   "cc-name": "John Doe",
   "cc-number": "4929001587121045",
   "cc-exp-month": 4,
   "cc-exp-year": 2017,
+  "cc-type": "visa",
 }, {
   "cc-name": "Timothy Berners-Lee",
   "cc-number": "5103059495477870",
   "cc-exp-month": 12,
   "cc-exp-year": 2022,
+  "cc-type": "mastercard",
 }];
 
 const reducedMockRecord = {
   "cc-name": "John Doe",
   "cc-number": "4929001587121045",
 };
 
 async function setupCreditCardStorage() {
@@ -192,15 +194,21 @@ add_task(async function check_form_autof
 <div id="content">
 
   <form id="form1">
     <p>This is a basic form.</p>
     <p><label>Name: <input id="cc-name" autocomplete="cc-name"></label></p>
     <p><label>Card Number: <input id="cc-number" autocomplete="cc-number"></label></p>
     <p><label>Expiration month: <input id="cc-exp-month" autocomplete="cc-exp-month"></label></p>
     <p><label>Expiration year: <input id="cc-exp-year" autocomplete="cc-exp-year"></label></p>
+    <p><label>Card Type: <select id="cc-type" autocomplete="cc-type">
+      <option value="discover">Discover</option>
+      <option value="jcb">JCB</option>
+      <option value="visa">Visa</option>
+      <option value="mastercard">MasterCard</option>
+    </select></label></p>
     <p><label>CSC: <input id="cc-csc" autocomplete="cc-csc"></label></p>
   </form>
 </div>
 
 <pre id="test"></pre>
 </body>
 </html>
--- a/browser/extensions/formautofill/test/unit/test_creditCardRecords.js
+++ b/browser/extensions/formautofill/test/unit/test_creditCardRecords.js
@@ -16,23 +16,25 @@ add_task(async function setup() {
 const TEST_STORE_FILE_NAME = "test-credit-card.json";
 const COLLECTION_NAME = "creditCards";
 
 const TEST_CREDIT_CARD_1 = {
   "cc-name": "John Doe",
   "cc-number": "4929001587121045",
   "cc-exp-month": 4,
   "cc-exp-year": 2017,
+  "cc-type": "visa",
 };
 
 const TEST_CREDIT_CARD_2 = {
   "cc-name": "Timothy Berners-Lee",
   "cc-number": "5103059495477870",
   "cc-exp-month": 12,
   "cc-exp-year": 2022,
+  "cc-type": "mastercard",
 };
 
 const TEST_CREDIT_CARD_3 = {
   "cc-number": "3589993783099582",
   "cc-exp-month": 1,
   "cc-exp-year": 2000,
 };
 
@@ -47,16 +49,17 @@ const TEST_CREDIT_CARD_WITH_BILLING_ADDR
   billingAddressGUID: "9m6hf4gfr6ge",
 };
 
 const TEST_CREDIT_CARD_WITH_EMPTY_FIELD = {
   billingAddressGUID: "",
   "cc-name": "",
   "cc-number": "344060747836806",
   "cc-exp-month": 1,
+  "cc-type": "",
 };
 
 const TEST_CREDIT_CARD_WITH_EMPTY_COMPUTED_FIELD = {
   "cc-given-name": "",
   "cc-additional-name": "",
   "cc-family-name": "",
   "cc-exp": "",
   "cc-number": "5415425865751454",
@@ -81,16 +84,24 @@ const TEST_CREDIT_CARD_WITH_INVALID_EXPI
   "cc-exp-year": -3,
 };
 
 const TEST_CREDIT_CARD_WITH_SPACES_BETWEEN_DIGITS = {
   "cc-name": "John Doe",
   "cc-number": "5103 0594 9547 7870",
 };
 
+const TEST_CREDIT_CARD_WITH_INVALID_NETWORK = {
+  "cc-name": "John Doe",
+  "cc-number": "4929001587121045",
+  "cc-exp-month": 4,
+  "cc-exp-year": 2017,
+  "cc-type": "asiv",
+};
+
 const TEST_CREDIT_CARD_EMPTY_AFTER_NORMALIZE = {
   "cc-exp-month": 13,
 };
 
 const TEST_CREDIT_CARD_EMPTY_AFTER_UPDATE_CREDIT_CARD_1 = {
   "cc-name": "",
   "cc-number": "",
   "cc-exp-month": 13,
@@ -393,16 +404,17 @@ add_task(async function test_update() {
   Assert.notEqual(creditCard.timeLastModified, timeLastModified);
   do_check_credit_card_matches(creditCard, TEST_CREDIT_CARD_3);
 
   // Empty string should be deleted while updating.
   profileStorage.creditCards.update(profileStorage.creditCards._data[0].guid, TEST_CREDIT_CARD_WITH_EMPTY_FIELD);
   creditCard = profileStorage.creditCards._data[0];
   Assert.equal(creditCard["cc-exp-month"], TEST_CREDIT_CARD_WITH_EMPTY_FIELD["cc-exp-month"]);
   Assert.equal(creditCard["cc-name"], undefined);
+  Assert.equal(creditCard["cc-type"], undefined);
   Assert.equal(creditCard.billingAddressGUID, undefined);
 
   // Empty computed fields shouldn't cause any problem.
   profileStorage.creditCards.update(profileStorage.creditCards._data[0].guid, TEST_CREDIT_CARD_WITH_EMPTY_COMPUTED_FIELD, false);
   creditCard = profileStorage.creditCards._data[0];
   Assert.equal(creditCard["cc-number"],
     CreditCard.getLongMaskedNumber(TEST_CREDIT_CARD_WITH_EMPTY_COMPUTED_FIELD["cc-number"]));
   profileStorage.creditCards.update(profileStorage.creditCards._data[1].guid, TEST_CREDIT_CARD_WITH_EMPTY_COMPUTED_FIELD, true);
@@ -441,30 +453,35 @@ add_task(async function test_validate() 
   let path = getTempFile(TEST_STORE_FILE_NAME).path;
 
   let profileStorage = new FormAutofillStorage(path);
   await profileStorage.initialize();
 
   profileStorage.creditCards.add(TEST_CREDIT_CARD_WITH_INVALID_EXPIRY_DATE);
   profileStorage.creditCards.add(TEST_CREDIT_CARD_WITH_2_DIGITS_YEAR);
   profileStorage.creditCards.add(TEST_CREDIT_CARD_WITH_SPACES_BETWEEN_DIGITS);
+  profileStorage.creditCards.add(TEST_CREDIT_CARD_WITH_INVALID_NETWORK);
 
   let creditCards = profileStorage.creditCards.getAll();
 
   Assert.equal(creditCards[0]["cc-exp-month"], undefined);
   Assert.equal(creditCards[0]["cc-exp-year"], undefined);
   Assert.equal(creditCards[0]["cc-exp"], undefined);
 
   let month = TEST_CREDIT_CARD_WITH_2_DIGITS_YEAR["cc-exp-month"];
   let year = parseInt(TEST_CREDIT_CARD_WITH_2_DIGITS_YEAR["cc-exp-year"], 10) + 2000;
   Assert.equal(creditCards[1]["cc-exp-month"], month);
   Assert.equal(creditCards[1]["cc-exp-year"], year);
   Assert.equal(creditCards[1]["cc-exp"], year + "-" + month.toString().padStart(2, "0"));
 
   Assert.equal(creditCards[2]["cc-number"].length, 16);
+
+  // dont enforce validity on the card network when storing a record,
+  // to avoid data loss when syncing records between different clients with different rules
+  Assert.equal(creditCards[3]["cc-type"], "asiv");
 });
 
 add_task(async function test_notifyUsed() {
   let path = getTempFile(TEST_STORE_FILE_NAME).path;
   await prepareTestCreditCards(path);
 
   let profileStorage = new FormAutofillStorage(path);
   await profileStorage.initialize();
--- a/browser/extensions/formautofill/test/unit/test_getRecords.js
+++ b/browser/extensions/formautofill/test/unit/test_getRecords.js
@@ -31,23 +31,25 @@ const TEST_ADDRESS_2 = {
   country: "US",
 };
 
 let TEST_CREDIT_CARD_1 = {
   "cc-name": "John Doe",
   "cc-number": "4111111111111111",
   "cc-exp-month": 4,
   "cc-exp-year": 2017,
+  "cc-type": "visa",
 };
 
 let TEST_CREDIT_CARD_2 = {
   "cc-name": "John Dai",
   "cc-number": "4929001587121045",
   "cc-exp-month": 2,
   "cc-exp-year": 2017,
+  "cc-type": "visa",
 };
 
 let target = {
   sendAsyncMessage: function sendAsyncMessage(msg, payload) {},
 };
 
 add_task(async function test_getRecords() {
   let formAutofillParent = new FormAutofillParent();
--- a/toolkit/modules/CreditCard.jsm
+++ b/toolkit/modules/CreditCard.jsm
@@ -4,47 +4,67 @@
 
 "use strict";
 
 var EXPORTED_SYMBOLS = ["CreditCard"];
 
 ChromeUtils.defineModuleGetter(this, "MasterPassword",
                                "resource://formautofill/MasterPassword.jsm");
 
+// The list of known and supported credit card network ids ("types")
+// This list mirrors the networks from dom/payments/BasicCardPayment.cpp
+// and is defined by https://www.w3.org/Payments/card-network-ids
+const SUPPORTED_NETWORKS = Object.freeze([
+  "amex",
+  "cartebancaire",
+  "diners",
+  "discover",
+  "jcb",
+  "mastercard",
+  "mir",
+  "unionpay",
+  "visa",
+]);
+
 class CreditCard {
   /**
    * @param {string} name
    * @param {string} number
    * @param {string} expirationString
    * @param {string|number} expirationMonth
    * @param {string|number} expirationYear
+   * @param {string} network
    * @param {string|number} ccv
    * @param {string} encryptedNumber
    */
   constructor({
     name,
     number,
     expirationString,
     expirationMonth,
     expirationYear,
+    network,
     ccv,
     encryptedNumber
   }) {
     this._name = name;
     this._unmodifiedNumber = number;
     this._encryptedNumber = encryptedNumber;
     this._ccv = ccv;
     this.number = number;
     // Only prefer the string version if missing one or both parsed formats.
     if (expirationString && (!expirationMonth || !expirationYear)) {
       this.expirationString = expirationString;
     } else {
       this.expirationMonth = expirationMonth;
       this.expirationYear = expirationYear;
     }
+    if (network) {
+      this.network = network;
+    }
   }
 
   set name(value) {
     this._name = value;
   }
 
   set expirationMonth(value) {
     if (typeof value == "undefined") {
@@ -91,16 +111,24 @@ class CreditCard {
       // 9 digits (Canadian SIN).
       // [1] https://en.wikipedia.org/wiki/Social_Insurance_Number
       normalizedNumber = normalizedNumber.match(/^\d{9,}$/) ?
         normalizedNumber : null;
       this._number = normalizedNumber;
     }
   }
 
+  get network() {
+    return this._network;
+  }
+
+  set network(value) {
+    this._network = value || undefined;
+  }
+
   // Implements the Luhn checksum algorithm as described at
   // http://wikipedia.org/wiki/Luhn_algorithm
   isValidNumber() {
     if (!this._number) {
       return false;
     }
 
     // Remove dashes and whitespace
@@ -287,9 +315,15 @@ class CreditCard {
     let creditCard = new CreditCard({number});
     return creditCard.longMaskedNumber;
   }
 
   static isValidNumber(number) {
     let creditCard = new CreditCard({number});
     return creditCard.isValidNumber();
   }
+
+  static isValidNetwork(network) {
+    return SUPPORTED_NETWORKS.includes(network);
+  }
 }
+CreditCard.SUPPORTED_NETWORKS = SUPPORTED_NETWORKS;
+
--- a/toolkit/modules/tests/xpcshell/test_CreditCard.js
+++ b/toolkit/modules/tests/xpcshell/test_CreditCard.js
@@ -267,8 +267,46 @@ add_task(async function test_label() {
     let card = new CreditCard({number, name});
 
     Assert.equal(await card.getLabel({showNumbers: true}), testCase.expectedLabel,
        "The expectedLabel should be shown when showNumbers is true");
     Assert.equal(await card.getLabel({showNumbers: false}), testCase.expectedMaskedLabel,
        "The expectedMaskedLabel should be shown when showNumbers is false");
   }
 });
+
+add_task(async function test_network() {
+  let supportedNetworks = CreditCard.SUPPORTED_NETWORKS;
+  Assert.ok(supportedNetworks.length, "There are > 0 supported credit card networks");
+
+  let ccNumber = "0000000000000000";
+  let testCases = supportedNetworks.map(network => {
+    return {number: ccNumber, network, expectedNetwork: network};
+  });
+  testCases.push({
+      number: ccNumber,
+      network: "gringotts",
+      expectedNetwork: "gringotts",
+  });
+  testCases.push({
+      number: ccNumber,
+      network: "",
+      expectedNetwork: undefined,
+  });
+
+  for (let testCase of testCases) {
+    let {number, network} = testCase;
+    let card = new CreditCard({number, network});
+    Assert.equal(await card.network, testCase.expectedNetwork,
+      `The expectedNetwork ${card.network} should match the card network ${testCase.expectedNetwork}`);
+  }
+});
+
+add_task(async function test_isValidNetwork() {
+  for (let network of CreditCard.SUPPORTED_NETWORKS) {
+    Assert.ok(CreditCard.isValidNetwork(network), "supported network is valid");
+  }
+  Assert.ok(!CreditCard.isValidNetwork(), "undefined is not a valid network");
+  Assert.ok(!CreditCard.isValidNetwork(""), "empty string is not a valid network");
+  Assert.ok(!CreditCard.isValidNetwork(null), "null is not a valid network");
+  Assert.ok(!CreditCard.isValidNetwork("Visa"), "network validity is case-sensitive");
+  Assert.ok(!CreditCard.isValidNetwork("madeupnetwork"), "unknown network is invalid");
+});