Repository: braintree/braintree-web-drop-in
Pull request #842 (2022-11-10 19:54:45)

fix: hide field errors and check for validity again on blur events
### Summary

Currently, we aren't removing card type supported errors if a user copy/pastes a supported card type number in.

![current](https://user-images.githubusercontent.com/4975234/201192523-67c4aade-e84b-4113-bf54-ce7d48d49221.gif)
(current behavior)

This PR hides field errors on blur events if the field is potentially valid and re-checks if the card type is supported or not.

![fix](https://user-images.githubusercontent.com/4975234/201193065-95d33b28-5af9-433d-8921-16dff2a2be02.gif)
(fixed behavior)

### Checklist

- [ ] Added a changelog entry

### Authors
> List GitHub usernames for everyone who contributed to this pull request.

- 



Patch:
diff --git a/src/views/payment-sheet-views/card-view.js b/src/views/payment-sheet-views/card-view.js
index 0cd9bf4..86c8c9b 100644
--- a/src/views/payment-sheet-views/card-view.js
+++ b/src/views/payment-sheet-views/card-view.js
@@ -447,11 +447,15 @@ CardView.prototype._onBlurEvent = function (event) {
 
   classList.remove(fieldGroup, 'braintree-form__field-group--is-focused');
 
+  if (field.isPotentiallyValid) {
+    this.hideFieldError(event.emittedBy);
+  }
+
   if (this._shouldApplyFieldEmptyError(event.emittedBy, field)) {
     this.showFieldError(event.emittedBy, this.strings['fieldEmptyFor' + capitalize(event.emittedBy)]);
   } else if (!field.isEmpty && !field.isValid) {
     this.showFieldError(event.emittedBy, this.strings['fieldInvalidFor' + capitalize(event.emittedBy)]);
-  } else if (event.emittedBy === 'number' && !this._isCardTypeSupported(event.cards[0].type)) {
+  } else if (event.emittedBy === 'number' && !this._isCardTypeSupported(event.cards[0])) {
     this.showFieldError('number', this.strings.unsupportedCardTypeError);
   }
 
@@ -489,6 +493,13 @@ CardView.prototype._onCardTypeChangeEvent = function (event) {
     classList.remove(numberFieldGroup, 'braintree-form__field-group--card-type-known');
   }
 
+  // if the number field emitted the card change event fires
+  // and the card type is supported, we need to clear out the errors
+  // field in case that there was a "card type is unsupported" error
+  if (event.emittedBy === 'number' && this._isCardTypeSupported(event.cards[0])) {
+    this.hideFieldError(event.emittedBy);
+  }
+
   this.cardNumberIconSvg.setAttribute('xlink:href', cardNumberHrefLink);
 
   if (this.hasCVV) {
@@ -526,7 +537,7 @@ CardView.prototype._onValidityChangeEvent = function (event) {
   var field = event.fields[event.emittedBy];
 
   if (event.emittedBy === 'number' && event.cards[0]) {
-    isValid = field.isValid && this._isCardTypeSupported(event.cards[0].type);
+    isValid = field.isValid && this._isCardTypeSupported(event.cards[0]);
   } else {
     isValid = field.isValid;
   }
@@ -578,7 +589,8 @@ CardView.prototype._hideUnsupportedCardIcons = function () {
   }.bind(this));
 };
 
-CardView.prototype._isCardTypeSupported = function (cardType) {
+CardView.prototype._isCardTypeSupported = function (card) {
+  var cardType = card && card.type;
   var configurationCardType = constants.configurationCardTypes[cardType];
   var supportedCardTypes = this.client.getConfiguration().gatewayConfiguration.creditCards.supportedCardTypes;
 


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 0da242a..b88a4c0 100644
--- a/test/unit/views/payment-sheet-views/card-view.js
+++ b/test/unit/views/payment-sheet-views/card-view.js
@@ -1086,6 +1086,34 @@ describe('CardView', () => {
         });
       });
 
+      test('removes field errors if field is potentially valid', () => {
+        eventPayload = {
+          emittedBy: 'number',
+          fields: {
+            number: {
+              isEmpty: false,
+              isPotentiallyValid: true,
+              isValid: false
+            }
+          }
+        };
+
+        client.getConfiguration.mockReturnValue({
+          gatewayConfiguration: {
+            challenges: ['cvv'],
+            creditCards: {
+              supportedCardTypes: ['Visa']
+            }
+          }
+        });
+
+        jest.spyOn(cardView, 'hideFieldError');
+
+        return cardView.initialize().then(() => {
+          expect(cardView.hideFieldError).toBeCalledTimes(1);
+        });
+      });
+
       test(
         'does apply error class if field is empty when focusing another hosted field',
         () => {
@@ -1300,6 +1328,73 @@ describe('CardView', () => {
         });
       });
 
+      test('hides field errors when card type changes and card type is supported', () => {
+        eventPayload = {
+          cards: [{ type: 'master-card' }],
+          emittedBy: 'number'
+        };
+
+        client.getConfiguration.mockReturnValue({
+          gatewayConfiguration: {
+            challenges: [],
+            creditCards: {
+              supportedCardTypes: ['MasterCard']
+            }
+          }
+        });
+
+        jest.spyOn(cardView, 'hideFieldError');
+
+        return cardView.initialize().then(() => {
+          expect(cardView.hideFieldError).toBeCalledTimes(1);
+          expect(cardView.hideFieldError).toBeCalledWith('number');
+        });
+      });
+
+      test('does not hide field errors when card type changes and card type is not supported', () => {
+        eventPayload = {
+          cards: [{ type: 'Maestro' }],
+          emittedBy: 'number'
+        };
+
+        client.getConfiguration.mockReturnValue({
+          gatewayConfiguration: {
+            challenges: [],
+            creditCards: {
+              supportedCardTypes: ['MasterCard']
+            }
+          }
+        });
+
+        jest.spyOn(cardView, 'hideFieldError');
+
+        return cardView.initialize().then(() => {
+          expect(cardView.hideFieldError).not.toBeCalled();
+        });
+      });
+
+      test('does not hide field errors when card did not emit card change event', () => {
+        eventPayload = {
+          cards: [{ type: 'MasterCard' }],
+          emittedBy: 'cvv'
+        };
+
+        client.getConfiguration.mockReturnValue({
+          gatewayConfiguration: {
+            challenges: [],
+            creditCards: {
+              supportedCardTypes: ['MasterCard']
+            }
+          }
+        });
+
+        jest.spyOn(cardView, 'hideFieldError');
+
+        return cardView.initialize().then(() => {
+          expect(cardView.hideFieldError).not.toBeCalled();
+        });
+      });
+
       test(
         'adds the card-type-known class when there is one possible card type',
         () => {