Repository: braintree/braintree-web-drop-in (js)

Issue:
Easier access to 3ds customer-canceled event
### Feature Request

When using the drop in ui with `threeDSecure: true`, it would be great if there was easier access to the `customer-canceled` event (when the user clicks cancel in the 3ds modal).

Currently I have to do this: 
```
braintreeInstance._threeDSecure._instance.on('customer-canceled', () => {...})
```

which feels like I am too deep into the internals.  It would be great if there was a a more direct path to attaching an event listener for this specific event.

Thanks!
Brandon

Pull request:
feat: add 3ds events
closes #690

### Summary

Adds 3ds events from core sdk. 

### Checklist

- [x] Added a changelog entry

### Authors
> List GitHub usernames for everyone who contributed to this pull request.

- @crookedneighbor 
- @jplukarski 


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3b81b96..8de0f09 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,8 +3,14 @@ CHANGELOG
 
 unreleased
 ----------
+- Fix issue where Drop-in may error or not set up completely due to dependency setup race condition (#700)
+- Fix issue where Drop-in may report it is ready even though no payment options are actually available
 - Google Pay
   - Fix issue where passing custom button option would caused a developer error in the console (#701)
+- 3D Secure
+  - Add `3ds:customer-canceled` event (#690)
+  - Add `3ds:authentication-modal-render` event
+  - Add `3ds:authentication-modal-close` event
 
 1.26.1
 ------
diff --git a/src/constants.js b/src/constants.js
index 7bb43ed..7be5cf8 100644
--- a/src/constants.js
+++ b/src/constants.js
@@ -44,6 +44,12 @@ module.exports = {
     unionpay: 'UnionPay',
     maestro: 'Maestro'
   },
+  dependencySetupStates: {
+    DONE: 'done',
+    FAILED: 'failed',
+    INITIALIZING: 'initializing',
+    NOT_ENABLED: 'not-enabled'
+  },
   errors: {
     NO_PAYMENT_METHOD_ERROR: 'No payment method is available.',
     DEVELOPER_MISCONFIGURATION_MESSAGE: 'Developer Error: Something went wrong. Check the console for details.'
diff --git a/src/dropin-model.js b/src/dropin-model.js
index 41006d7..cd28589 100644
--- a/src/dropin-model.js
+++ b/src/dropin-model.js
@@ -6,6 +6,7 @@ var EventEmitter = require('@braintree/event-emitter');
 var constants = require('./constants');
 var paymentMethodTypes = constants.paymentMethodTypes;
 var paymentOptionIDs = constants.paymentOptionIDs;
+var dependencySetupStates = constants.dependencySetupStates;
 var isGuestCheckout = require('./lib/is-guest-checkout');
 var Promise = require('./lib/promise');
 var paymentSheetViews = require('./views/payment-sheet-views');
@@ -24,16 +25,28 @@ var DEFAULT_PAYMENT_OPTION_PRIORITY = [
   paymentOptionIDs.applePay,
   paymentOptionIDs.googlePay
 ];
+var ASYNC_DEPENDENCIES = DEFAULT_PAYMENT_OPTION_PRIORITY.concat(['threeDSecure', 'dataCollector']);
+var DEPENDENCY_READY_CHECK_INTERVAL = 200;
 
 function DropinModel(options) {
   this.rootNode = options.container;
   this.componentID = options.componentID;
   this.merchantConfiguration = options.merchantConfiguration;
-
   this.isGuestCheckout = isGuestCheckout(options.client);
 
-  this.dependenciesInitializing = 0;
-  this.dependencySuccessCount = 0;
+  this.dependencyStates = ASYNC_DEPENDENCIES.reduce(function (total, dependencyKey) {
+    if (dependencyKey in options.merchantConfiguration) {
+      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;
+  }
+
   this.failedDependencies = {};
   this._options = options;
   this._setupComplete = false;
@@ -50,13 +63,30 @@ EventEmitter.createChild(DropinModel);
 
 DropinModel.prototype.initialize = function () {
   var self = this;
+  var dependencyReadyInterval = setInterval(function () {
+    var ready = true;
+
+    Object.keys(self.dependencyStates).forEach(function (dep) {
+      if (self.dependencyStates[dep] === dependencySetupStates.INITIALIZING) {
+        ready = false;
+      }
+    });
+
+    if (!ready) {
+      return;
+    }
+
+    clearInterval(dependencyReadyInterval);
+
+    self._emit('asyncDependenciesReady');
+  }, DEPENDENCY_READY_CHECK_INTERVAL);
 
   return vaultManager.create({
     client: self._options.client
   }).then(function (vaultManagerInstance) {
     self._vaultManager = vaultManagerInstance;
 
-    return getSupportedPaymentOptions(self._options);
+    return self._getSupportedPaymentOptions(self._options);
   }).then(function (paymentOptions) {
     self.supportedPaymentOptions = paymentOptions;
 
@@ -219,14 +249,21 @@ DropinModel.prototype.reportAppSwitchError = function (sheetId, error) {
   };
 };
 
-DropinModel.prototype.asyncDependencyStarting = function () {
-  this.dependenciesInitializing++;
+DropinModel.prototype.hasAtLeastOneAvailablePaymentOption = function () {
+  var self = this;
+  var i;
+
+  for (i = 0; i < this.supportedPaymentOptions.length; i++) {
+    if (self.dependencyStates[this.supportedPaymentOptions[i]] === dependencySetupStates.DONE) {
+      return true;
+    }
+  }
+
+  return false;
 };
 
-DropinModel.prototype.asyncDependencyReady = function () {
-  this.dependencySuccessCount++;
-  this.dependenciesInitializing--;
-  this._checkAsyncDependencyFinished();
+DropinModel.prototype.asyncDependencyReady = function (key) {
+  this.dependencyStates[key] = dependencySetupStates.DONE;
 };
 
 DropinModel.prototype.asyncDependencyFailed = function (options) {
@@ -234,14 +271,7 @@ DropinModel.prototype.asyncDependencyFailed = function (options) {
     return;
   }
   this.failedDependencies[options.view] = options.error;
-  this.dependenciesInitializing--;
-  this._checkAsyncDependencyFinished();
-};
-
-DropinModel.prototype._checkAsyncDependencyFinished = function () {
-  if (this.dependenciesInitializing === 0) {
-    this._emit('asyncDependenciesReady');
-  }
+  this.dependencyStates[options.view] = dependencySetupStates.FAILED;
 };
 
 DropinModel.prototype.cancelInitialization = function (error) {
@@ -329,7 +359,8 @@ DropinModel.prototype._getSupportedPaymentMethods = function (paymentMethods) {
   });
 };
 
-function getSupportedPaymentOptions(options) {
+DropinModel.prototype._getSupportedPaymentOptions = function (options) {
+  var self = this;
   var paymentOptionPriority = options.merchantConfiguration.paymentOptionPriority || DEFAULT_PAYMENT_OPTION_PRIORITY;
   var promises;
 
@@ -341,7 +372,13 @@ function getSupportedPaymentOptions(options) {
   paymentOptionPriority = paymentOptionPriority.filter(function (item, pos) { return paymentOptionPriority.indexOf(item) === pos; });
 
   promises = paymentOptionPriority.map(function (paymentOption) {
-    return getPaymentOption(paymentOption, options);
+    return getPaymentOption(paymentOption, options).then(function (result) {
+      if (!result.success) {
+        self.dependencyStates[result.id] = dependencySetupStates.NOT_ENABLED;
+      }
+
+      return result;
+    });
   });
 
   return Promise.all(promises).then(function (result) {
@@ -355,7 +392,7 @@ function getSupportedPaymentOptions(options) {
 
     return result.map(function (item) { return item.id; });
   });
-}
+};
 
 function getPaymentOption(paymentOption, options) {
   return isPaymentOptionEnabled(paymentOption, options).then(function (success) {
diff --git a/src/dropin.js b/src/dropin.js
index 881f937..b5aead3 100644
--- a/src/dropin.js
+++ b/src/dropin.js
@@ -38,7 +38,12 @@ var PASS_THROUGH_EVENTS = [
   'card:focus',
   'card:inputSubmitRequest',
   'card:notEmpty',
-  'card:validityChange'
+  'card:validityChange',
+
+  // 3DS Events
+  '3ds:customer-canceled',
+  '3ds:authentication-modal-render',
+  '3ds:authentication-modal-close'
 ];
 var UPDATABLE_CONFIGURATION_OPTIONS = [
   paymentOptionIDs.paypal,
@@ -165,6 +170,11 @@ HAS_RAW_PAYMENT_DATA[constants.paymentMethodTypes.applePay] = true;
  *  * [`card:inputSubmitRequest`](#event:card:inputSubmitRequest)
  *  * [`card:notEmpty`](#event:card:notEmpty)
  *  * [`card:validityChange`](#event:card:validityChange)
+ *
+ *  _3DS Specific Events_
+ *  * [`3ds:customer-canceled`](#event:3ds:customer-canceled)
+ *  * [`3ds:authentication-modal-render`](#event:3ds:authentication-modal-render)
+ *  * [`3ds:authentication-modal-close`](#event:3ds:authentication-modal-close)
  * @returns {void}
  * @example
  * <caption>Dynamically enable or disable your submit button based on whether or not the payment method is requestable</caption>
@@ -349,6 +359,24 @@ HAS_RAW_PAYMENT_DATA[constants.paymentMethodTypes.applePay] = true;
  * @type {Dropin~card:validityChange}
  */
 
+/**
+ * The underlying [3D Secure `customer-canceled` event](http://braintree.github.io/braintree-web/{@pkg bt-web-version}/ThreeDSecure.html#event:customer-canceled).
+ * @event Dropin#3ds:customer-canceled
+ * @type {Dropin~3ds:customer-canceled}
+ */
+
+/**
+ * The underlying [3D Secure `authentication-modal-render` event](http://braintree.github.io/braintree-web/{@pkg bt-web-version}/ThreeDSecure.html#event:authentication-modal-render).
+ * @event Dropin#3ds:authentication-modal-render
+ * @type {Dropin~3ds:authentication-modal-render}
+ */
+
+/**
+ * The underlying [3D Secure `authentication-modal-close` event](http://braintree.github.io/braintree-web/{@pkg bt-web-version}/ThreeDSecure.html#event:authentication-modal-close).
+ * @event Dropin#3ds:authentication-modal-close
+ * @type {Dropin~3ds:authentication-modal-close}
+ */
+
 /**
  * @typedef {object} Dropin~paymentOptionSelectedPayload
  * @description The event payload sent from {@link Dropin#on|`on`} with the {@link Dropin#event:paymentOptionSelected|`paymentOptionSelected`} event.
@@ -456,7 +484,7 @@ Dropin.prototype._initialize = function (callback) {
     });
 
     self._model.on('asyncDependenciesReady', function () {
-      if (self._model.dependencySuccessCount >= 1) {
+      if (self._model.hasAtLeastOneAvailablePaymentOption()) {
         analytics.sendEvent(self._client, 'appeared');
         self._disableErroredPaymentMethods();
 
@@ -597,11 +625,10 @@ Dropin.prototype._setUpDataCollector = function () {
   var self = this;
   var config = assign({}, self._merchantConfiguration.dataCollector, {client: self._client});
 
-  this._model.asyncDependencyStarting();
   this._dataCollector = new DataCollector(config);
 
   this._dataCollector.initialize().then(function () {
-    self._model.asyncDependencyReady();
+    self._model.asyncDependencyReady('dataCollector');
   }).catch(function (err) {
     self._model.cancelInitialization(new DropinError({
       message: 'Data Collector failed to set up.',
@@ -613,12 +640,10 @@ Dropin.prototype._setUpDataCollector = function () {
 Dropin.prototype._setUpThreeDSecure = function () {
   var self = this;
 
-  this._model.asyncDependencyStarting();
-
   this._threeDSecure = new ThreeDSecure(this._client, this._model);
 
   this._threeDSecure.initialize().then(function () {
-    self._model.asyncDependencyReady();
+    self._model.asyncDependencyReady('threeDSecure');
   }).catch(function (err) {
     self._model.cancelInitialization(new DropinError({
       message: '3D Secure failed to set up.',
diff --git a/src/lib/three-d-secure.js b/src/lib/three-d-secure.js
index 77cd0ea..bf55b45 100644
--- a/src/lib/three-d-secure.js
+++ b/src/lib/three-d-secure.js
@@ -4,9 +4,15 @@ var assign = require('./assign').assign;
 var threeDSecure = require('braintree-web/three-d-secure');
 
 var DEFAULT_ACS_WINDOW_SIZE = '03';
+var PASSTHROUGH_EVENTS = [
+  'customer-canceled',
+  'authentication-modal-render',
+  'authentication-modal-close'
+];
 
 function ThreeDSecure(client, model) {
   this._client = client;
+  this._model = model;
   this._config = assign({}, model.merchantConfiguration.threeDSecure);
 }
 
@@ -18,6 +24,12 @@ ThreeDSecure.prototype.initialize = function () {
     version: 2
   }).then(function (instance) {
     self._instance = instance;
+
+    PASSTHROUGH_EVENTS.forEach(function (eventName) {
+      self._instance.on(eventName, function (event) {
+        self._model._emit('3ds:' + eventName, event);
+      });
+    });
   });
 };
 
diff --git a/src/views/payment-sheet-views/apple-pay-view.js b/src/views/payment-sheet-views/apple-pay-view.js
index 45d5877..548923b 100644
--- a/src/views/payment-sheet-views/apple-pay-view.js
+++ b/src/views/payment-sheet-views/apple-pay-view.js
@@ -26,8 +26,6 @@ ApplePayView.prototype.initialize = function () {
 
   delete self.applePayConfiguration.applePaySessionVersion;
 
-  self.model.asyncDependencyStarting();
-
   return btApplePay.create({client: this.client}).then(function (applePayInstance) {
     var buttonDiv = self.getElementById('apple-pay-button');
 
@@ -36,7 +34,7 @@ ApplePayView.prototype.initialize = function () {
     buttonDiv.onclick = self._showPaymentSheet.bind(self);
     buttonDiv.style['-apple-pay-button-style'] = self.model.merchantConfiguration.applePay.buttonStyle || 'black';
 
-    self.model.asyncDependencyReady();
+    self.model.asyncDependencyReady(ApplePayView.ID);
   }).catch(function (err) {
     self.model.asyncDependencyFailed({
       view: self.ID,
diff --git a/src/views/payment-sheet-views/base-paypal-view.js b/src/views/payment-sheet-views/base-paypal-view.js
index 6ddb6ce..a26ce38 100644
--- a/src/views/payment-sheet-views/base-paypal-view.js
+++ b/src/views/payment-sheet-views/base-paypal-view.js
@@ -34,7 +34,6 @@ BasePayPalView.prototype.initialize = function () {
 
   this.paypalConfiguration = assign({}, paypalConfiguration);
 
-  this.model.asyncDependencyStarting();
   asyncDependencyTimeoutHandler = setTimeout(function () {
     self.model.asyncDependencyFailed({
       view: self.ID,
@@ -94,7 +93,7 @@ BasePayPalView.prototype.initialize = function () {
     buttonSelector = dropinWrapperId + ' ' + buttonSelector;
 
     return global.paypal.Button.render(checkoutJSConfiguration, buttonSelector).then(function () {
-      self.model.asyncDependencyReady();
+      self.model.asyncDependencyReady(paypalType);
       setupComplete = true;
       clearTimeout(asyncDependencyTimeoutHandler);
     });
diff --git a/src/views/payment-sheet-views/card-view.js b/src/views/payment-sheet-views/card-view.js
index 63b8a2a..3870ac6 100644
--- a/src/views/payment-sheet-views/card-view.js
+++ b/src/views/payment-sheet-views/card-view.js
@@ -73,8 +73,6 @@ CardView.prototype.initialize = function () {
     this.saveCardInput.checked = false;
   }
 
-  this.model.asyncDependencyStarting();
-
   return hostedFields.create(hfOptions).then(function (hostedFieldsInstance) {
     this.hostedFieldsInstance = hostedFieldsInstance;
     this.hostedFieldsInstance.on('blur', this._onBlurEvent.bind(this));
@@ -89,7 +87,7 @@ CardView.prototype.initialize = function () {
       }.bind(this));
     }.bind(this));
 
-    this.model.asyncDependencyReady();
+    this.model.asyncDependencyReady(CardView.ID);
   }.bind(this)).catch(function (err) {
     this.model.asyncDependencyFailed({
       view: this.ID,
diff --git a/src/views/payment-sheet-views/google-pay-view.js b/src/views/payment-sheet-views/google-pay-view.js
index a25e449..735b769 100644
--- a/src/views/payment-sheet-views/google-pay-view.js
+++ b/src/views/payment-sheet-views/google-pay-view.js
@@ -43,8 +43,6 @@ GooglePayView.prototype.initialize = function () {
   delete self.googlePayConfiguration.merchantId;
   delete self.googlePayConfiguration.button;
 
-  self.model.asyncDependencyStarting();
-
   return btGooglePay.create({
     client: self.client,
     googlePayVersion: googlePayVersion,
@@ -57,7 +55,7 @@ GooglePayView.prototype.initialize = function () {
 
     buttonContainer.appendChild(self.paymentsClient.createButton(buttonOptions));
 
-    self.model.asyncDependencyReady();
+    self.model.asyncDependencyReady(GooglePayView.ID);
   }).catch(function (err) {
     self.model.asyncDependencyFailed({
       view: self.ID,
diff --git a/src/views/payment-sheet-views/venmo-view.js b/src/views/payment-sheet-views/venmo-view.js
index 5b7ccc0..1fd673d 100644
--- a/src/views/payment-sheet-views/venmo-view.js
+++ b/src/views/payment-sheet-views/venmo-view.js
@@ -19,8 +19,6 @@ VenmoView.prototype.initialize = function () {
   var self = this;
   var venmoConfiguration = assign({}, self.model.merchantConfiguration.venmo, {client: this.client});
 
-  self.model.asyncDependencyStarting();
-
   return btVenmo.create(venmoConfiguration).then(function (venmoInstance) {
     self.venmoInstance = venmoInstance;
 
@@ -57,7 +55,7 @@ VenmoView.prototype.initialize = function () {
       });
     });
 
-    self.model.asyncDependencyReady();
+    self.model.asyncDependencyReady(VenmoView.ID);
   }).catch(function (err) {
     self.model.asyncDependencyFailed({
       view: self.ID,


Test patch:
diff --git a/test/helpers/fake.js b/test/helpers/fake.js
index b22ea39..05025ec 100644
--- a/test/helpers/fake.js
+++ b/test/helpers/fake.js
@@ -86,7 +86,8 @@ fakeBTInstances = {
   threeDSecure: {
     verifyCard: function () {},
     cancelVerifyCard: function () {},
-    teardown: function () {}
+    teardown: function () {},
+    on: function () {}
   }
 };
 
diff --git a/test/unit/dropin-model.js b/test/unit/dropin-model.js
index 0773f08..48f8857 100644
--- a/test/unit/dropin-model.js
+++ b/test/unit/dropin-model.js
@@ -142,6 +142,44 @@ describe('DropinModel', () => {
   });
 
   describe('initialize', () => {
+    test('emits asyncDependenciesReady event when no dependencies are set to initializing', (done) => {
+      jest.useFakeTimers();
+
+      const model = new DropinModel(testContext.modelOptions);
+
+      jest.spyOn(model, '_emit');
+
+      model.on('asyncDependenciesReady', () => {
+        jest.useRealTimers();
+
+        done();
+      });
+
+      return model.initialize().then(() => {
+        expect(model._emit).not.toBeCalledWith('asyncDependenciesReady');
+
+        model.asyncDependencyReady('paypal');
+
+        expect(model._emit).not.toBeCalledWith('asyncDependenciesReady');
+
+        jest.advanceTimersByTime(1000);
+
+        expect(model._emit).not.toBeCalledWith('asyncDependenciesReady');
+
+        model.asyncDependencyReady('venmo');
+
+        expect(model._emit).not.toBeCalledWith('asyncDependenciesReady');
+
+        jest.advanceTimersByTime(1000);
+
+        model.asyncDependencyReady('card');
+
+        expect(model._emit).not.toBeCalledWith('asyncDependenciesReady');
+
+        jest.advanceTimersByTime(1000);
+      });
+    });
+
     test('creates a vault manager', () => {
       const model = new DropinModel(testContext.modelOptions);
 
@@ -187,6 +225,7 @@ describe('DropinModel', () => {
         expect(model._paymentMethods).toEqual([
           { type: 'CreditCard', details: { lastTwo: '11' }, vaulted: true }
         ]);
+        expect(model.dependencyStates.paypal).toBe('not-enabled');
       });
     });
 
@@ -579,6 +618,38 @@ describe('DropinModel', () => {
     });
   });
 
+  describe('hasAtLeastOneAvailablePaymentOption', () => {
+    test('returns false when no payment methods have a setup status of done', async () => {
+      // all dependencies will have a setup status of initializing
+      const model = new DropinModel(testContext.modelOptions);
+
+      await model.initialize();
+
+      expect(model.hasAtLeastOneAvailablePaymentOption()).toBe(false);
+    });
+
+    test('returns true when at least one payment method has a setup status of done', async () => {
+      const model = new DropinModel(testContext.modelOptions);
+
+      await model.initialize();
+
+      model.asyncDependencyReady('card');
+
+      expect(model.hasAtLeastOneAvailablePaymentOption()).toBe(true);
+    });
+
+    test('returns false when no payment methods have a setup status of done, even if non-payment methods do', async () => {
+      const model = new DropinModel(testContext.modelOptions);
+
+      await model.initialize();
+
+      model.asyncDependencyReady('dataCollector');
+      model.asyncDependencyReady('threeDSecure');
+
+      expect(model.hasAtLeastOneAvailablePaymentOption()).toBe(false);
+    });
+  });
+
   describe('reportAppSwitchPayload', () => {
     test('saves app switch payload to instance', () => {
       const model = new DropinModel(testContext.modelOptions);
@@ -602,22 +673,20 @@ describe('DropinModel', () => {
     });
   });
 
-  describe('asyncDependencyStarting', () => {
+  describe('asyncDependencyFailed', () => {
     beforeEach(() => {
-      testContext.context = {
-        dependenciesInitializing: 0
-      };
+      testContext.model = new DropinModel(testContext.modelOptions);
     });
 
-    test('increments dependenciesInitializing by one', () => {
-      DropinModel.prototype.asyncDependencyStarting.call(testContext.context);
-      expect(testContext.context.dependenciesInitializing).toBe(1);
-    });
-  });
+    test('marks depenedency as failed', () => {
+      const err = new Error('a bad error');
 
-  describe('asyncDependencyFailed', () => {
-    beforeEach(() => {
-      testContext.model = new DropinModel(testContext.modelOptions);
+      expect(testContext.model.dependencyStates.venmo).toBe('initializing');
+      testContext.model.asyncDependencyFailed({
+        view: 'venmo',
+        error: err
+      });
+      expect(testContext.model.dependencyStates.venmo).toBe('failed');
     });
 
     test('adds an error to the failedDependencies object', () => {
@@ -656,10 +725,13 @@ describe('DropinModel', () => {
           done();
         });
 
-        model.asyncDependencyStarting();
-        model.asyncDependencyFailed({
-          view: 'id',
-          error: new Error('fake error')
+        model.initialize().then(() => {
+          model.asyncDependencyReady('paypal');
+          model.asyncDependencyFailed({
+            view: 'venmo',
+            error: new Error('fake error')
+          });
+          model.asyncDependencyReady('card');
         });
       }
     );
@@ -670,14 +742,27 @@ describe('DropinModel', () => {
       testContext.context = { callback: jest.fn() };
     });
 
-    test('decrements dependenciesInitializing by one', () => {
+    test('marks initializing dependency as done', () => {
       const model = new DropinModel(testContext.modelOptions);
 
-      model.dependenciesInitializing = 2;
+      // the modelOptions have a config for paypal and venmo
+      expect(model.dependencyStates.paypal).toBe('initializing');
+      expect(model.dependencyStates.venmo).toBe('initializing');
+
+      model.asyncDependencyReady('paypal');
+      expect(model.dependencyStates.paypal).toBe('done');
+      expect(model.dependencyStates.venmo).toBe('initializing');
+      expect(model.dependencyStates.card).toBe('initializing');
 
-      model.asyncDependencyReady();
+      model.asyncDependencyReady('venmo');
+      expect(model.dependencyStates.paypal).toBe('done');
+      expect(model.dependencyStates.venmo).toBe('done');
+      expect(model.dependencyStates.card).toBe('initializing');
 
-      expect(model.dependenciesInitializing).toBe(1);
+      model.asyncDependencyReady('card');
+      expect(model.dependencyStates.paypal).toBe('done');
+      expect(model.dependencyStates.venmo).toBe('done');
+      expect(model.dependencyStates.card).toBe('done');
     });
 
     test(
@@ -688,30 +773,34 @@ describe('DropinModel', () => {
         jest.spyOn(DropinModel.prototype, 'asyncDependencyReady');
 
         model.on('asyncDependenciesReady', () => {
-          expect(DropinModel.prototype.asyncDependencyReady).toBeCalledTimes(1);
+          expect(DropinModel.prototype.asyncDependencyReady).toBeCalledTimes(3);
           done();
         });
 
-        model.asyncDependencyStarting();
-        model.asyncDependencyReady();
+        model.initialize().then(() => {
+          model.asyncDependencyReady('paypal');
+          model.asyncDependencyReady('venmo');
+          model.asyncDependencyReady('card');
+        });
       }
     );
 
-    test('emits asyncDependenciesReady event with prior errors', () => {
+    test('emits asyncDependenciesReady event with prior errors', (done) => {
       const model = new DropinModel(testContext.modelOptions);
       const err = new Error('an earlier dependency failed');
 
-      jest.spyOn(model, '_emit');
-
-      model.asyncDependencyStarting();
-      model.asyncDependencyStarting();
-      model.asyncDependencyFailed({
-        view: 'id',
-        error: err
+      model.on('asyncDependenciesReady', () => {
+        done();
       });
-      model.asyncDependencyReady();
 
-      expect(model._emit).toBeCalledWith('asyncDependenciesReady');
+      return model.initialize().then(() => {
+        model.asyncDependencyReady('card');
+        model.asyncDependencyFailed({
+          view: 'venmo',
+          error: err
+        });
+        model.asyncDependencyReady('paypal');
+      });
     });
   });
 
diff --git a/test/unit/dropin.js b/test/unit/dropin.js
index b4d6ec9..01facbb 100644
--- a/test/unit/dropin.js
+++ b/test/unit/dropin.js
@@ -487,17 +487,7 @@ describe('Dropin', () => {
       done => {
         const instance = new Dropin(testContext.dropinOptions);
 
-        jest.spyOn(DropinModel.prototype, 'asyncDependencyStarting').mockImplementation();
-        jest.spyOn(DropinModel.prototype, 'asyncDependencyReady').mockImplementation();
-
-        instance._initialize(err => {
-          done(err);
-        });
-
-        delay().then(() => {
-          instance._model.dependencySuccessCount = 1;
-          instance._model._emit('asyncDependenciesReady');
-        });
+        instance._initialize(done);
       }
     );
 
@@ -505,8 +495,6 @@ describe('Dropin', () => {
       const instance = new Dropin(testContext.dropinOptions);
       const error = new Error('error');
 
-      jest.spyOn(DropinModel.prototype, 'asyncDependencyStarting').mockImplementation();
-      jest.spyOn(DropinModel.prototype, 'asyncDependencyReady').mockImplementation();
       jest.spyOn(DropinModel.prototype, 'reportError').mockImplementation();
 
       instance._initialize(() => {
@@ -518,13 +506,11 @@ describe('Dropin', () => {
       });
 
       delay().then(() => {
-        instance._model.dependencySuccessCount = 1;
         instance._model.appSwitchError = {
           id: 'view-id',
           error: error
         };
         jest.spyOn(instance._mainView, 'setPrimaryView').mockImplementation();
-        instance._model._emit('asyncDependenciesReady');
       });
     });
 
@@ -532,8 +518,6 @@ describe('Dropin', () => {
       const instance = new Dropin(testContext.dropinOptions);
       const payload = { nonce: 'fake-nonce' };
 
-      jest.spyOn(DropinModel.prototype, 'asyncDependencyStarting').mockImplementation();
-      jest.spyOn(DropinModel.prototype, 'asyncDependencyReady').mockImplementation();
       jest.spyOn(DropinModel.prototype, 'addPaymentMethod').mockImplementation();
 
       instance._initialize(() => {
@@ -542,11 +526,7 @@ describe('Dropin', () => {
         done();
       });
 
-      delay().then(() => {
-        instance._model.dependencySuccessCount = 1;
-        instance._model.appSwitchPayload = payload;
-        instance._model._emit('asyncDependenciesReady');
-      });
+      instance._model.appSwitchPayload = payload;
     });
 
     test(
@@ -554,19 +534,11 @@ describe('Dropin', () => {
       done => {
         const instance = new Dropin(testContext.dropinOptions);
 
-        jest.spyOn(DropinModel.prototype, 'asyncDependencyStarting').mockImplementation();
-        jest.spyOn(DropinModel.prototype, 'asyncDependencyReady').mockImplementation();
-
         instance._initialize(() => {
           expect(analytics.sendEvent).toBeCalledTimes(1);
           expect(analytics.sendEvent).toBeCalledWith(instance._client, 'appeared');
           done();
         });
-
-        delay().then(() => {
-          instance._model.dependencySuccessCount = 2;
-          instance._model._emit('asyncDependenciesReady');
-        });
       }
     );
 
@@ -617,8 +589,6 @@ describe('Dropin', () => {
           { type: 'CreditCard', details: { lastTwo: '11' }},
           { type: 'PayPalAccount', details: { email: 'wow@example.com' }}
         ]);
-        jest.spyOn(DropinModel.prototype, 'asyncDependencyStarting').mockImplementation();
-        jest.spyOn(DropinModel.prototype, 'asyncDependencyReady').mockImplementation();
 
         instance._initialize(() => {
           expect(analytics.sendEvent).not.toBeCalledWith(instance._client, 'vaulted-card.appear');
@@ -628,13 +598,10 @@ describe('Dropin', () => {
         });
 
         delay().then(() => {
-          instance._model.dependencySuccessCount = 1;
           instance._model.appSwitchPayload = {
             nonce: 'a-nonce'
           };
           jest.spyOn(instance._mainView, 'setPrimaryView').mockImplementation();
-
-          instance._model._emit('asyncDependenciesReady');
         });
       }
     );
@@ -648,8 +615,6 @@ describe('Dropin', () => {
           { type: 'CreditCard', details: { lastTwo: '11' }},
           { type: 'PayPalAccount', details: { email: 'wow@example.com' }}
         ]);
-        jest.spyOn(DropinModel.prototype, 'asyncDependencyStarting').mockImplementation();
-        jest.spyOn(DropinModel.prototype, 'asyncDependencyReady').mockImplementation();
 
         instance._initialize(() => {
           expect(analytics.sendEvent).not.toBeCalledWith(instance._client, 'vaulted-card.appear');
@@ -659,14 +624,11 @@ describe('Dropin', () => {
         });
 
         delay().then(() => {
-          instance._model.dependencySuccessCount = 1;
           instance._model.appSwitchError = {
             ID: 'view',
             error: new Error('error')
           };
           jest.spyOn(instance._mainView, 'setPrimaryView').mockImplementation();
-
-          instance._model._emit('asyncDependenciesReady');
         });
       }
     );
@@ -812,32 +774,48 @@ describe('Dropin', () => {
   describe('loads data collector', () => {
     beforeEach(() => {
       testContext.dropinOptions.merchantConfiguration.dataCollector = { kount: true };
-      jest.spyOn(DropinModel.prototype, 'asyncDependencyStarting');
       jest.spyOn(DropinModel.prototype, 'asyncDependencyReady');
-      jest.spyOn(Dropin.prototype, '_setUpDataCollector').mockImplementation();
+      jest.spyOn(DataCollector.prototype, 'initialize').mockResolvedValue();
     });
 
-    test(
-      'does not load Data Collector if Data Collector is not enabled',
-      done => {
-        let instance;
+    test('does not load Data Collector if Data Collector is not enabled', done => {
+      let instance;
 
-        delete testContext.dropinOptions.merchantConfiguration.dataCollector;
-        instance = new Dropin(testContext.dropinOptions);
+      delete testContext.dropinOptions.merchantConfiguration.dataCollector;
+      instance = new Dropin(testContext.dropinOptions);
 
-        instance._initialize(() => {
-          expect(instance._setUpDataCollector).not.toBeCalled();
+      instance._initialize(() => {
+        expect(DataCollector.prototype.initialize).not.toBeCalled();
+        expect(DropinModel.prototype.asyncDependencyReady).not.toBeCalledWith('dataCollector');
 
-          done();
-        });
-      }
-    );
+        done();
+      });
+    });
 
     test('does load Data Collector if Data Collector is enabled', done => {
       const instance = new Dropin(testContext.dropinOptions);
 
       instance._initialize(() => {
-        expect(instance._setUpDataCollector).toBeCalled();
+        expect(DataCollector.prototype.initialize).toBeCalledTimes(1);
+        expect(DropinModel.prototype.asyncDependencyReady).toBeCalledWith('dataCollector');
+
+        done();
+      });
+    });
+
+    test('cancels initialization when Data Collection creation fails', done => {
+      const error = new Error('failed.');
+
+      jest.spyOn(DropinModel.prototype, 'cancelInitialization');
+
+      DataCollector.prototype.initialize.mockRejectedValue(error);
+
+      const instance = new Dropin(testContext.dropinOptions);
+
+      instance._initialize(() => {
+        expect(DataCollector.prototype.initialize).toBeCalledTimes(1);
+        expect(DropinModel.prototype.asyncDependencyReady).not.toBeCalledWith('dataCollector');
+        expect(DropinModel.prototype.cancelInitialization).toBeCalledTimes(1);
 
         done();
       });
@@ -847,9 +825,8 @@ describe('Dropin', () => {
   describe('loads 3D Secure', () => {
     beforeEach(() => {
       testContext.dropinOptions.merchantConfiguration.threeDSecure = {};
-      jest.spyOn(DropinModel.prototype, 'asyncDependencyStarting');
       jest.spyOn(DropinModel.prototype, 'asyncDependencyReady');
-      jest.spyOn(Dropin.prototype, '_setUpThreeDSecure');
+      jest.spyOn(ThreeDSecure.prototype, 'initialize').mockResolvedValue();
     });
 
     test('does not load 3D Secure if 3D Secure is not enabled', done => {
@@ -859,7 +836,8 @@ describe('Dropin', () => {
       instance = new Dropin(testContext.dropinOptions);
 
       instance._initialize(() => {
-        expect(instance._setUpThreeDSecure).not.toBeCalled();
+        expect(ThreeDSecure.prototype.initialize).not.toBeCalled();
+        expect(DropinModel.prototype.asyncDependencyReady).not.toBeCalledWith('threeDSecure');
 
         done();
       });
@@ -869,114 +847,12 @@ describe('Dropin', () => {
       const instance = new Dropin(testContext.dropinOptions);
 
       instance._initialize(() => {
-        expect(instance._setUpThreeDSecure).toBeCalled();
+        expect(ThreeDSecure.prototype.initialize).toBeCalled();
+        expect(DropinModel.prototype.asyncDependencyReady).toBeCalledWith('threeDSecure');
 
         done();
       });
     });
-  });
-
-  describe('_setUpDataCollector', () => {
-    beforeEach(() => {
-      testContext.model = fake.model({
-        componentID: 'foo',
-        client: testContext.client,
-        merchantConfiguration: {
-          container: '#foo',
-          authorization: fake.tokenizationKey
-        },
-        paymentMethods: ['card']
-      });
-      jest.spyOn(DataCollector.prototype, 'initialize').mockResolvedValue();
-    });
-
-    test('sets up datacollector', () => {
-      Dropin.prototype._setUpDataCollector.call({
-        _client: testContext.client,
-        _model: testContext.model,
-        _strings: {},
-        _merchantConfiguration: {
-          dataCollector: {
-            kount: true
-          }
-        }
-      });
-
-      expect(DataCollector.prototype.initialize).toBeCalledTimes(1);
-    });
-
-    test(
-      'fails initialization when Data Collection creation fails',
-      done => {
-        const error = new Error('failed.');
-
-        jest.spyOn(DropinModel.prototype, 'cancelInitialization');
-
-        DataCollector.prototype.initialize.mockRejectedValue(error);
-
-        Dropin.prototype._setUpDataCollector.call({
-          _client: testContext.client,
-          _model: testContext.model,
-          _merchantConfiguration: {
-            threeDSecure: {
-              foo: 'bar'
-            }
-          }
-        });
-
-        expect(DataCollector.prototype.initialize).toBeCalledTimes(1);
-        setTimeout(() => {
-          expect(DropinModel.prototype.cancelInitialization).toBeCalled();
-          done();
-        }, 1000);
-      }
-    );
-
-    test('starts an async dependency', () => {
-      function noop() {}
-      jest.spyOn(DropinModel.prototype, 'asyncDependencyStarting');
-
-      Dropin.prototype._setUpDataCollector.call({
-        _client: testContext.client,
-        _merchantConfiguration: {
-          threeDSecure: {
-            foo: 'bar'
-          }
-        },
-        _model: testContext.model
-      }, noop);
-
-      expect(DropinModel.prototype.asyncDependencyStarting).toBeCalledTimes(1);
-    });
-  });
-
-  describe('_setUpThreeDSecure', () => {
-    beforeEach(() => {
-      testContext.model = fake.model({
-        componentID: 'foo',
-        client: testContext.client,
-        merchantConfiguration: {
-          container: '#foo',
-          authorization: fake.tokenizationKey
-        },
-        paymentMethods: ['card']
-      });
-      jest.spyOn(ThreeDSecure.prototype, 'initialize').mockResolvedValue();
-    });
-
-    test('sets up 3ds', () => {
-      Dropin.prototype._setUpThreeDSecure.call({
-        _client: testContext.client,
-        _model: testContext.model,
-        _merchantConfiguration: {
-          threeDSecure: {
-            foo: 'bar'
-          }
-        }
-      });
-
-      expect(ThreeDSecure.prototype.initialize).toBeCalledTimes(1);
-    });
 
     test('fails initialization when 3DS creation fails', done => {
       const error = new Error('failed.');
@@ -985,38 +861,13 @@ describe('Dropin', () => {
 
       ThreeDSecure.prototype.initialize.mockRejectedValue(error);
 
-      Dropin.prototype._setUpThreeDSecure.call({
-        _client: testContext.client,
-        _model: testContext.model,
-        _merchantConfiguration: {
-          threeDSecure: {
-            foo: 'bar'
-          }
-        }
-      });
+      const instance = new Dropin(testContext.dropinOptions);
 
-      expect(ThreeDSecure.prototype.initialize).toBeCalledTimes(1);
-      setTimeout(() => {
+      instance._initialize(() => {
+        expect(ThreeDSecure.prototype.initialize).toBeCalledTimes(1);
         expect(DropinModel.prototype.cancelInitialization).toBeCalled();
         done();
-      }, 1000);
-    });
-
-    test('starts an async dependency', () => {
-      function noop() {}
-      jest.spyOn(DropinModel.prototype, 'asyncDependencyStarting');
-
-      Dropin.prototype._setUpThreeDSecure.call({
-        _client: testContext.client,
-        _merchantConfiguration: {
-          threeDSecure: {
-            foo: 'bar'
-          }
-        },
-        _model: testContext.model
-      }, noop);
-
-      expect(DropinModel.prototype.asyncDependencyStarting).toBeCalledTimes(1);
+      });
     });
   });
 
@@ -2447,6 +2298,26 @@ describe('Dropin', () => {
     });
   });
 
+  describe('3ds events', () => {
+    test.each([
+      'customer-canceled',
+      'authentication-modal-render',
+      'authentication-modal-close'
+    ])('emits 3ds:%s event', (eventName, done) => {
+      const instance = new Dropin(testContext.dropinOptions);
+      const payload = {};
+
+      instance.on(`3ds:${eventName}`, (emittedPayload) => {
+        expect(emittedPayload).toBe(payload);
+        done();
+      });
+
+      instance._initialize(() => {
+        instance._model._emit(`3ds:${eventName}`, payload);
+      });
+    });
+  });
+
   describe('payment option selected event', () => {
     test(
       'emits paymentOptionSelected when the model emits paymentOptionSelected',
diff --git a/test/unit/lib/three-d-secure.js b/test/unit/lib/three-d-secure.js
index dde927f..5024f4b 100644
--- a/test/unit/lib/three-d-secure.js
+++ b/test/unit/lib/three-d-secure.js
@@ -25,6 +25,7 @@ describe('ThreeDSecure', () => {
     });
     jest.spyOn(testContext.threeDSecureInstance, 'verifyCard').mockImplementation();
     jest.spyOn(testContext.threeDSecureInstance, 'cancelVerifyCard').mockImplementation();
+    jest.spyOn(testContext.threeDSecureInstance, 'on').mockImplementation();
 
     jest.spyOn(classList, 'add').mockImplementation();
     jest.spyOn(classList, 'remove').mockImplementation();
@@ -48,6 +49,22 @@ describe('ThreeDSecure', () => {
         expect(tds._instance).toBe(testContext.threeDSecureInstance);
       });
     });
+
+    test('adds event listeners for 3ds specific events', () => {
+      const client = {};
+      const tds = new ThreeDSecure(client, testContext.model);
+
+      testContext.threeDSecureInstance.on.mockImplementation((eventName, cb) => {
+        cb({ someEvent: 'foo' });
+      });
+
+      return tds.initialize().then(() => {
+        expect(testContext.model._emit).toBeCalledTimes(3);
+        expect(testContext.model._emit).toBeCalledWith('3ds:customer-canceled', { someEvent: 'foo' });
+        expect(testContext.model._emit).toBeCalledWith('3ds:authentication-modal-render', { someEvent: 'foo' });
+        expect(testContext.model._emit).toBeCalledWith('3ds:authentication-modal-close', { someEvent: 'foo' });
+      });
+    });
   });
 
   describe('verify', () => {
diff --git a/test/unit/views/payment-methods-view.js b/test/unit/views/payment-methods-view.js
index a880b37..6c0afde 100644
--- a/test/unit/views/payment-methods-view.js
+++ b/test/unit/views/payment-methods-view.js
@@ -153,6 +153,7 @@ describe('PaymentMethodsView', () => {
         model.getVaultedPaymentMethods.mockResolvedValue([{ foo: 'bar' }, fakePaymentMethod]);
 
         return model.initialize().then(() => {
+          model.asyncDependencyReady('card');
           model.changeActivePaymentMethod({ foo: 'bar' });
 
           paymentMethodsViews = new PaymentMethodsView({
diff --git a/test/unit/views/payment-sheet-views/apple-pay-view.js b/test/unit/views/payment-sheet-views/apple-pay-view.js
index 683cef2..15876e9 100644
--- a/test/unit/views/payment-sheet-views/apple-pay-view.js
+++ b/test/unit/views/payment-sheet-views/apple-pay-view.js
@@ -80,19 +80,12 @@ describe('ApplePayView', () => {
       testContext.view = new ApplePayView(testContext.applePayViewOptions);
     });
 
-    test('starts async dependency', () => {
-      jest.spyOn(testContext.view.model, 'asyncDependencyStarting').mockImplementation();
-
-      return testContext.view.initialize().then(() => {
-        expect(testContext.view.model.asyncDependencyStarting).toBeCalledTimes(1);
-      });
-    });
-
     test('notifies async dependency', () => {
       jest.spyOn(testContext.view.model, 'asyncDependencyReady').mockImplementation();
 
       return testContext.view.initialize().then(() => {
         expect(testContext.view.model.asyncDependencyReady).toBeCalledTimes(1);
+        expect(testContext.view.model.asyncDependencyReady).toBeCalledWith('applePay');
       });
     });
 
diff --git a/test/unit/views/payment-sheet-views/base-paypal-view.js b/test/unit/views/payment-sheet-views/base-paypal-view.js
index 1ff0358..27e7f93 100644
--- a/test/unit/views/payment-sheet-views/base-paypal-view.js
+++ b/test/unit/views/payment-sheet-views/base-paypal-view.js
@@ -80,19 +80,24 @@ describe('BasePayPalView', () => {
       testContext.view = new BasePayPalView(testContext.paypalViewOptions);
     });
 
-    test('starts async dependency', () => {
-      jest.spyOn(testContext.view.model, 'asyncDependencyStarting').mockImplementation();
+    test('notifies async dependency ready for paypal', () => {
+      jest.spyOn(testContext.view.model, 'asyncDependencyReady').mockImplementation();
 
       return testContext.view.initialize().then(() => {
-        expect(testContext.view.model.asyncDependencyStarting).toBeCalledTimes(1);
+        expect(testContext.view.model.asyncDependencyReady).toBeCalledTimes(1);
+        expect(testContext.view.model.asyncDependencyReady).toBeCalledWith('paypal');
       });
     });
 
-    test('notifies async dependency', () => {
+    test('notifies async dependency ready for paypalCredit', () => {
       jest.spyOn(testContext.view.model, 'asyncDependencyReady').mockImplementation();
 
+      testContext.view.model.merchantConfiguration.paypalCredit = testContext.view.model.merchantConfiguration.paypal;
+      testContext.view._isPayPalCredit = true;
+
       return testContext.view.initialize().then(() => {
         expect(testContext.view.model.asyncDependencyReady).toBeCalledTimes(1);
+        expect(testContext.view.model.asyncDependencyReady).toBeCalledWith('paypalCredit');
       });
     });
 
@@ -134,19 +139,6 @@ describe('BasePayPalView', () => {
       }
     );
 
-    test('calls asyncDependencyStarting when initializing', () => {
-      const fakeError = {
-        code: 'A_REAL_ERROR_CODE'
-      };
-
-      PayPalCheckout.create.mockRejectedValue(fakeError);
-
-      jest.spyOn(DropinModel.prototype, 'asyncDependencyStarting').mockImplementation();
-      testContext.view.initialize();
-
-      expect(testContext.view.model.asyncDependencyStarting).toBeCalledTimes(1);
-    });
-
     test('calls paypal.Button.render', () => {
       return testContext.view.initialize().then(() => {
         expect(testContext.paypal.Button.render).toBeCalledTimes(1);
diff --git a/test/unit/views/payment-sheet-views/card-view.js b/test/unit/views/payment-sheet-views/card-view.js
index c2d1ca1..293cf9d 100644
--- a/test/unit/views/payment-sheet-views/card-view.js
+++ b/test/unit/views/payment-sheet-views/card-view.js
@@ -319,21 +319,6 @@ describe('CardView', () => {
       });
     });
 
-    test('starts async dependency', () => {
-      jest.spyOn(DropinModel.prototype, 'asyncDependencyStarting');
-
-      const view = new CardView({
-        element: cardElement,
-        model: fakeModel,
-        client: fakeClient,
-        strings: strings
-      });
-
-      return view.initialize().then(() => {
-        expect(DropinModel.prototype.asyncDependencyStarting).toBeCalledTimes(1);
-      });
-    });
-
     test(
       'notifies async dependency is ready when Hosted Fields is created',
       () => {
@@ -348,6 +333,7 @@ describe('CardView', () => {
 
         return view.initialize().then(() => {
           expect(DropinModel.prototype.asyncDependencyReady).toBeCalledTimes(1);
+          expect(DropinModel.prototype.asyncDependencyReady).toBeCalledWith('card');
         });
       }
     );
diff --git a/test/unit/views/payment-sheet-views/google-pay-view.js b/test/unit/views/payment-sheet-views/google-pay-view.js
index 481a835..2f9bfc2 100644
--- a/test/unit/views/payment-sheet-views/google-pay-view.js
+++ b/test/unit/views/payment-sheet-views/google-pay-view.js
@@ -107,19 +107,12 @@ describe('GooglePayView', () => {
       testContext.view = new GooglePayView(testContext.googlePayViewOptions);
     });
 
-    test('starts async dependency', () => {
-      jest.spyOn(testContext.view.model, 'asyncDependencyStarting').mockImplementation();
-
-      return testContext.view.initialize().then(() => {
-        expect(testContext.view.model.asyncDependencyStarting).toBeCalledTimes(1);
-      });
-    });
-
     test('notifies async dependency', () => {
       jest.spyOn(testContext.view.model, 'asyncDependencyReady').mockImplementation();
 
       return testContext.view.initialize().then(() => {
         expect(testContext.view.model.asyncDependencyReady).toBeCalledTimes(1);
+        expect(testContext.view.model.asyncDependencyReady).toBeCalledWith('googlePay');
       });
     });
 
diff --git a/test/unit/views/payment-sheet-views/venmo-view.js b/test/unit/views/payment-sheet-views/venmo-view.js
index f62b255..9cb42dd 100644
--- a/test/unit/views/payment-sheet-views/venmo-view.js
+++ b/test/unit/views/payment-sheet-views/venmo-view.js
@@ -61,19 +61,12 @@ describe('VenmoView', () => {
       testContext.view = new VenmoView(testContext.venmoViewOptions);
     });
 
-    test('starts async dependency', () => {
-      jest.spyOn(testContext.view.model, 'asyncDependencyStarting').mockImplementation();
-
-      return testContext.view.initialize().then(() => {
-        expect(testContext.view.model.asyncDependencyStarting).toBeCalledTimes(1);
-      });
-    });
-
     test('notifies async dependency', () => {
       jest.spyOn(testContext.view.model, 'asyncDependencyReady').mockImplementation();
 
       return testContext.view.initialize().then(() => {
         expect(testContext.view.model.asyncDependencyReady).toBeCalledTimes(1);
+        expect(testContext.view.model.asyncDependencyReady).toBeCalledWith('venmo');
       });
     });