Repository: braintree/braintree-web-drop-in
Pull request #885 (2023-05-22 21:38:40)

Fix: Removing card icons if they are overridden
### Summary

Currently, if a card type is overridden in the `card.overrides.fields.number.supportedCardBrands` object, the buyer sees an error message during card validation, but the card icon is still being shown. This change adds the `braintree-hidden` class to any card type that's being overridden by the merchant.

Ticket for internal tracking: https://paypal.atlassian.net/browse/LI-10689

![2023-05-10_12-46-21 (1)](https://github.com/braintree/braintree-web-drop-in/assets/63573634/f90412f8-fe4a-43d0-9e68-9804d34bd6d0)

### Checklist

- [ ] Added a changelog entry

### Authors
> List GitHub usernames for everyone who contributed to this pull request.

- 


SancSalix: Don't forget a changelog entry!

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d000753..0153e4f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,8 @@
 # CHANGELOG
 
+## Unreleased
+  - Fix issue where supportedCardBrands overrides were incorrectly showing images for hidden card brands
+
 ## 1.37.0
   - Drop dependency on `promise-polyfill`
   - Drop dependency on @braintree/class-list
diff --git a/src/constants.js b/src/constants.js
index ecee81d..aaaf44c 100644
--- a/src/constants.js
+++ b/src/constants.js
@@ -37,6 +37,19 @@ module.exports = {
     Hiper: 'hiper',
     Hipercard: 'hipercard'
   },
+  cardTypeIcons: {
+    visa: 'visa',
+    mastercard: 'master-card',
+    'american-express': 'american-express',
+    'diners-club': 'diners-club',
+    discover: 'discover',
+    jcb: 'jcb',
+    'union-pay': 'unionpay',
+    maestro: 'maestro',
+    elo: 'elo',
+    hiper: 'hiper',
+    hipercard: 'hipercard'
+  },
   configurationCardTypes: {
     visa: 'Visa',
     'master-card': 'MasterCard',
diff --git a/src/views/payment-sheet-views/card-view.js b/src/views/payment-sheet-views/card-view.js
index d4612b9..8b6dd4a 100644
--- a/src/views/payment-sheet-views/card-view.js
+++ b/src/views/payment-sheet-views/card-view.js
@@ -29,7 +29,6 @@ CardView.ID = CardView.prototype.ID = constants.paymentOptionIDs.card;
 CardView.prototype.initialize = function () {
   var cvvFieldGroup, postalCodeFieldGroup, hfOptions;
   var cardholderNameGroup = this.getElementById('cardholder-name-field-group');
-  var cardIcons = this.getElementById('card-view-icons');
 
   // If merchant explicty passes a value of `true` for card configuration,
   // we need to treat it as if no card configuration was passed, and provide
@@ -44,7 +43,7 @@ CardView.prototype.initialize = function () {
   this.cardholderNameRequired = this.hasCardholderName && this.merchantConfiguration.cardholderName.required === true;
   hfOptions = this._generateHostedFieldsOptions();
 
-  cardIcons.innerHTML = cardIconHTML;
+  this._renderCardIcons();
   this._hideUnsupportedCardIcons();
 
   this.hasCVV = hfOptions.fields.cvv;
@@ -114,6 +113,27 @@ CardView.prototype._sendRequestableEvent = function () {
   }
 };
 
+CardView.prototype._renderCardIcons = function () {
+  var overrides = this.merchantConfiguration.overrides;
+  var cardIcons = this.getElementById('card-view-icons');
+  var supportedCardBrands = overrides && overrides.fields && overrides.fields.number && overrides.fields.number.supportedCardBrands;
+
+  cardIcons.innerHTML = cardIconHTML;
+
+  if (supportedCardBrands) {
+    Object.keys(supportedCardBrands).forEach(function (cardBrand) {
+      var value = supportedCardBrands[cardBrand];
+      var selector, iconDiv;
+
+      if (value === false) {
+        selector = 'div[data-braintree-id="' + constants.cardTypeIcons[cardBrand] + '-card-icon"]';
+        iconDiv = document.querySelector(selector);
+        hideCardIcon(iconDiv);
+      }
+    });
+  }
+};
+
 CardView.prototype._generateHostedFieldsOptions = function () {
   var challenges = this.client.getConfiguration().gatewayConfiguration.challenges;
   var hasCVVChallenge = challenges.indexOf('cvv') !== -1;
@@ -614,6 +634,12 @@ CardView.prototype._shouldApplyFieldEmptyError = function (fieldId, field) {
   return isCardViewElement();
 };
 
+function hideCardIcon(icon) {
+  if (icon) {
+    icon.classList.add('braintree-hidden');
+  }
+}
+
 function isCardViewElement() {
   var activeId = document.activeElement && document.activeElement.id;
   var isHostedFieldsElement = document.activeElement instanceof HTMLIFrameElement && activeId.indexOf('braintree-hosted-field') !== -1;


Test patch:
diff --git a/test/unit/views/payment-sheet-views/card-view.js b/test/unit/views/payment-sheet-views/card-view.js
index 7ccdad0..9b6343a 100644
--- a/test/unit/views/payment-sheet-views/card-view.js
+++ b/test/unit/views/payment-sheet-views/card-view.js
@@ -13,6 +13,7 @@ const {
 
 const mainHTML = fs.readFileSync(__dirname + '/../../../../src/html/main.html', 'utf8');
 const CHANGE_ACTIVE_PAYMENT_METHOD_TIMEOUT = require('../../../../src/constants').CHANGE_ACTIVE_PAYMENT_METHOD_TIMEOUT;
+const constants = require('../../../../src/constants');
 
 describe('CardView', () => {
   let fakeClient, fakeHostedFieldsInstance, cardElement, container;
@@ -506,7 +507,10 @@ describe('CardView', () => {
         strings: strings
       });
 
+      jest.spyOn(view, '_renderCardIcons');
+
       return view.initialize().then(() => {
+        expect(view._renderCardIcons).toBeCalledTimes(1);
         supportedCardTypes.forEach(cardType => {
           const cardIcon = cardElement.querySelector('[data-braintree-id="' + cardType + '-card-icon"]');
 
@@ -534,6 +538,87 @@ describe('CardView', () => {
       });
     });
 
+    test('hides card icons if they are overriden', () => {
+      fakeModel.merchantConfiguration.card = {
+        overrides: {
+          fields: {
+            number: {
+              supportedCardBrands: {
+                'american-express': false,
+                jcb: false
+              }
+            }
+          }
+        }
+      };
+
+      const view = new CardView({
+        element: cardElement,
+        model: fakeModel,
+        client: fakeClient,
+        strings: strings
+      });
+
+      jest.spyOn(view, '_renderCardIcons');
+
+      return view.initialize().then(() => {
+        expect(view._renderCardIcons).toBeCalledTimes(1);
+        const amexIcon = cardElement.querySelector('[data-braintree-id="' + constants.cardTypeIcons['american-express'] + '-card-icon"]');
+        const jcbIcon = cardElement.querySelector('[data-braintree-id="' + constants.cardTypeIcons.jcb + '-card-icon"]');
+
+        expect(amexIcon.classList.contains('braintree-hidden')).toBe(true);
+        expect(jcbIcon.classList.contains('braintree-hidden')).toBe(true);
+      });
+    });
+
+    test('does not error if overridden icons is empty', () => {
+      fakeModel.merchantConfiguration.card = {
+        overrides: {}
+      };
+
+      const view = new CardView({
+        element: cardElement,
+        model: fakeModel,
+        client: fakeClient,
+        strings: strings
+      });
+
+      jest.spyOn(view, '_renderCardIcons');
+
+      return view.initialize().then(() => {
+        expect(view._renderCardIcons).toBeCalledTimes(1);
+        expect(view._renderCardIcons).toReturn();
+      });
+    });
+
+    test('does not error if merchant passes in an unknown card vendor to card overrides', () => {
+      fakeModel.merchantConfiguration.card = {
+        overrides: {
+          fields: {
+            number: {
+              supportedCardBrands: {
+                'unknown-card-vendor': false
+              }
+            }
+          }
+        }
+      };
+
+      const view = new CardView({
+        element: cardElement,
+        model: fakeModel,
+        client: fakeClient,
+        strings: strings
+      });
+
+      jest.spyOn(view, '_renderCardIcons');
+
+      return view.initialize().then(() => {
+        expect(view._renderCardIcons).toBeCalledTimes(1);
+        expect(view._renderCardIcons).toReturn();
+      });
+    });
+
     test('does not show Elo icon even if it is supported', () => {
       let eloCardIcon;