Repository: braintree/braintree-web-drop-in
Pull request #740 (2021-06-02 21:24:00)

Payment option priority fix
### Summary

Fix issue where Drop-In does not load if `paymentOptionPrioriry` is passed in ( #739 )

### Checklist

- [X] Added a changelog entry

### Authors
@crookedneighbor 
@jplukarski 



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 59dee01..5b405de 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@ CHANGELOG
 UNRELEASED
 ----------
 - Fix issue where cursor didn't change for Apple Pay on hover (#737)
+- Fix issue where Drop-in couldn't load if `paymentOptionPriority` is set (#739)
 
 1.29.0
 ------
diff --git a/src/dropin-model.js b/src/dropin-model.js
index 327812d..071f7c0 100644
--- a/src/dropin-model.js
+++ b/src/dropin-model.js
@@ -25,7 +25,8 @@ var DEFAULT_PAYMENT_OPTION_PRIORITY = [
   paymentOptionIDs.applePay,
   paymentOptionIDs.googlePay
 ];
-var ASYNC_DEPENDENCIES = DEFAULT_PAYMENT_OPTION_PRIORITY.concat(['threeDSecure', 'dataCollector']);
+var NON_PAYMENT_OPTION_DEPENDENCIES = ['threeDSecure', 'dataCollector'];
+var ASYNC_DEPENDENCIES = DEFAULT_PAYMENT_OPTION_PRIORITY.concat(NON_PAYMENT_OPTION_DEPENDENCIES);
 var DEPENDENCY_READY_CHECK_INTERVAL = 200;
 
 function DropinModel(options) {
@@ -33,19 +34,13 @@ function DropinModel(options) {
   this.componentID = options.componentID;
   this.merchantConfiguration = options.merchantConfiguration;
   this.isGuestCheckout = isGuestCheckout(options.client);
-
   this.dependencyStates = ASYNC_DEPENDENCIES.reduce(function (total, dependencyKey) {
-    if (options.merchantConfiguration[dependencyKey]) {
+    if (this._shouldIncludeDependency(dependencyKey)) {
       total[dependencyKey] = dependencySetupStates.INITIALIZING;
     }
 
     return total;
-  }, {});
-  // card is on by default, so we need to set it's state to INITIALIZING
-  // unless the merchant has specifically opted out of using the card form
-  if (options.merchantConfiguration.card !== false) {
-    this.dependencyStates.card = dependencySetupStates.INITIALIZING;
-  }
+  }.bind(this), {});
 
   this.failedDependencies = {};
   this._options = options;
@@ -176,6 +171,40 @@ DropinModel.prototype.confirmPaymentMethodDeletion = function (paymentMethod) {
   this._emit('confirmPaymentMethodDeletion', paymentMethod);
 };
 
+DropinModel.prototype._shouldIncludeDependency = function (key) {
+  if (key === 'card') {
+    // card is turned on by default unless the merchant explicitly
+    // passes a value of `false` or omits it from a custom
+    // `paymentOptionPriority` array
+    if (this.merchantConfiguration.card === false) {
+      return false;
+    }
+  } else if (!this.merchantConfiguration[key]) {
+    // if the merchant does not have the non-card dependency
+    // configured, do not include the dependency
+    return false;
+  }
+
+  if (NON_PAYMENT_OPTION_DEPENDENCIES.indexOf(key) > -1) {
+    console.log(key);
+    console.log('it got here');
+    // if the dependency is not a payment option (3DS, data collector)
+    // include it since the merchant configured Drop-in for it
+
+    return true;
+  }
+
+  if (this.merchantConfiguration.paymentOptionPriority) {
+    // if the merchant passed a custom `paymentOptionPriority` array,
+    // only include the dependency if it was configured _and_
+    // included in the array
+    return this.merchantConfiguration.paymentOptionPriority.indexOf(key) > -1;
+  }
+
+  // otherwise, include it if it is a valid payment option
+  return DEFAULT_PAYMENT_OPTION_PRIORITY.indexOf(key) > -1;
+};
+
 DropinModel.prototype._shouldEmitRequestableEvent = function (options) {
   var requestableStateHasNotChanged = this.isPaymentMethodRequestable() === options.isRequestable;
   var nonce = options.selectedPaymentMethod && options.selectedPaymentMethod.nonce;


Test patch:
diff --git a/test/integration/payment-option-priority.test.js b/test/integration/payment-option-priority.test.js
new file mode 100644
index 0000000..aca9330
--- /dev/null
+++ b/test/integration/payment-option-priority.test.js
@@ -0,0 +1,56 @@
+describe('Drop-in with paymentOptionPriority', function () {
+  beforeEach(function () {
+    browser.reloadSessionOnRetry(this.currentTest);
+  });
+
+  it('displays payment methods in order of the payment option priority array @paypal', function () {
+    browser.start({
+      paypal: 'default',
+      paypalCredit: 'default',
+      venmo: 'default',
+      paymentOptionPriority: ['paypal', 'card', 'venmo', 'paypalCredit']
+    });
+
+    const options = $$('.braintree-options-list .braintree-option');
+
+    expect(options.length).toBe(4);
+    expect(options[0].getAttribute('class')).toContain('braintree-option__paypal');
+    expect(options[1].getAttribute('class')).toContain('braintree-option__card');
+    expect(options[2].getAttribute('class')).toContain('braintree-option__venmo');
+    expect(options[3].getAttribute('class')).toContain('braintree-option__paypalCredit');
+  });
+
+  it('displays only payment methods in the payment option priority array, even when configured @paypal', function () {
+    browser.start({
+      paypal: 'default',
+      paypalCredit: 'default',
+      venmo: 'default',
+      paymentOptionPriority: ['card', 'venmo']
+    });
+
+    expect($$('.braintree-option').length).toBe(2);
+    expect($('.braintree-option__paypal').isDisplayed()).toBeFalsy();
+    expect($('.braintree-option__paypalCredit').isDisplayed()).toBeFalsy();
+    expect($('.braintree-option__venmo').isDisplayed()).toBeTruthy();
+    expect($('.braintree-option__card').isDisplayed()).toBeTruthy();
+  });
+
+  it('displays the payment method sheet when only a single payment method is presented in paymentOptionPriority @paypal', function () {
+    browser.start({
+      paypal: 'default',
+      paypalCredit: 'default',
+      venmo: 'default',
+      paymentOptionPriority: ['paypal']
+    });
+
+    expect($('.braintree-option__paypal').isDisplayed()).toBeFalsy();
+    expect($('.braintree-option__paypalCredit').isDisplayed()).toBeFalsy();
+    expect($('.braintree-option__venmo').isDisplayed()).toBeFalsy();
+    expect($('.braintree-option__card').isDisplayed()).toBeFalsy();
+    expect($('[data-braintree-id="paypal"]').isDisplayed()).toBeTruthy();
+
+    $('.braintree-sheet__button--paypal iframe.zoid-visible').waitForExist();
+
+    expect($('.braintree-sheet__button--paypal iframe.zoid-visible').isDisplayed()).toBeTruthy();
+  });
+});
diff --git a/test/unit/dropin-model.js b/test/unit/dropin-model.js
index 1ae25c4..1f14777 100644
--- a/test/unit/dropin-model.js
+++ b/test/unit/dropin-model.js
@@ -110,6 +110,56 @@ describe('DropinModel', () => {
       expect(model.dependencyStates.paypal).toBe('initializing');
     });
 
+    it('does not set card as initializing when merchant configuration has falsy value', () => {
+      testContext.modelOptions.merchantConfiguration.card = false;
+
+      const model = new DropinModel(testContext.modelOptions);
+
+      expect(model.dependencyStates.card).toBeFalsy();
+    });
+
+    it('does not set card as initializing when it is not specified in the merchant configuration and not in the payment option priority', () => {
+      testContext.modelOptions.merchantConfiguration.paymentOptionPriority = ['paypal', 'paypalCredit', 'googlePay', 'applePay', 'venmo'];
+
+      const model = new DropinModel(testContext.modelOptions);
+
+      expect(model.dependencyStates.card).toBeFalsy();
+    });
+
+    it('does not set payment option as initializing when n and not in the payment option priority', () => {
+      testContext.modelOptions.merchantConfiguration.card = true;
+      testContext.modelOptions.merchantConfiguration.venmo = true;
+      testContext.modelOptions.merchantConfiguration.paypal = true;
+      testContext.modelOptions.merchantConfiguration.googlePay = true;
+      testContext.modelOptions.merchantConfiguration.paymentOptionPriority = ['paypal', 'venmo'];
+
+      const model = new DropinModel(testContext.modelOptions);
+
+      expect(model.dependencyStates.card).toBeFalsy();
+      expect(model.dependencyStates.googlePay).toBeFalsy();
+      expect(model.dependencyStates.paypal).toBeTruthy();
+      expect(model.dependencyStates.venmo).toBeTruthy();
+    });
+
+    it('does set data collector and 3DS as initializing when a payment option priority is configured', () => {
+      testContext.modelOptions.merchantConfiguration.card = true;
+      testContext.modelOptions.merchantConfiguration.venmo = true;
+      testContext.modelOptions.merchantConfiguration.paypal = true;
+      testContext.modelOptions.merchantConfiguration.googlePay = true;
+      testContext.modelOptions.merchantConfiguration.dataCollector = true;
+      testContext.modelOptions.merchantConfiguration.threeDSecure = true;
+      testContext.modelOptions.merchantConfiguration.paymentOptionPriority = ['paypal', 'venmo'];
+
+      const model = new DropinModel(testContext.modelOptions);
+
+      expect(model.dependencyStates.card).toBeFalsy();
+      expect(model.dependencyStates.googlePay).toBeFalsy();
+      expect(model.dependencyStates.paypal).toBeTruthy();
+      expect(model.dependencyStates.venmo).toBeTruthy();
+      expect(model.dependencyStates.dataCollector).toBeTruthy();
+      expect(model.dependencyStates.threeDSecure).toBeTruthy();
+    });
+
     describe('isGuestCheckout', () => {
       test('is true when given a tokenization key', () => {
         let model;
@@ -368,6 +418,23 @@ describe('DropinModel', () => {
       });
     });
 
+    test('ignores configured payment methods that are not present in a custom paymentOptionPriority array', () => {
+      let model;
+
+      testContext.configuration.gatewayConfiguration.paypalEnabled = true;
+      testContext.modelOptions.merchantConfiguration.paypal = true;
+      testContext.modelOptions.merchantConfiguration.venmo = true;
+      testContext.modelOptions.merchantConfiguration.applePay = true;
+      testContext.modelOptions.merchantConfiguration.googlePay = true;
+      testContext.modelOptions.merchantConfiguration.paymentOptionPriority = ['paypal', 'card'];
+
+      model = new DropinModel(testContext.modelOptions);
+
+      return model.initialize().then(() => {
+        expect(model.supportedPaymentOptions).toEqual(['paypal', 'card']);
+      });
+    });
+
     test(
       'calls isEnabled on payment method view to determine if payment method is available',
       () => {