diff --git a/CHANGELOG.md b/CHANGELOG.md index e9ea3f2e..5c6587fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,10 @@ # CHANGELOG ## UNRELEASED - - Apple Pay: add error message prompting the customer to click the Apple Pay button when `requestPaymentMethod` is called. + - Apple Pay + - add error message prompting the customer to click the Apple Pay button when `requestPaymentMethod` is called. + - 3D Secure + - Fix issue where `paymentMethodRequestable` event would fire before 3DS challenge has been completed. (closes [#805](https://github.com/braintree/braintree-web-drop-in/issues/805)) ## 1.41.0 - Update braintree-web to 3.97.4 diff --git a/src/dropin-model.js b/src/dropin-model.js index ef063cff..5b923cec 100644 --- a/src/dropin-model.js +++ b/src/dropin-model.js @@ -52,6 +52,7 @@ function DropinModel(options) { this.failedDependencies = {}; this._options = options; this._setupComplete = false; + this.shouldWaitForVerifyCard = false; while (this.rootNode.parentNode) { this.rootNode = this.rootNode.parentNode; @@ -223,6 +224,10 @@ DropinModel.prototype._shouldEmitRequestableEvent = function (options) { return false; } + if (this.shouldWaitForVerifyCard) { + return false; + } + if (requestableStateHasNotChanged && (!options.isRequestable || nonceHasNotChanged)) { return false; } diff --git a/src/dropin.js b/src/dropin.js index 6c024736..b5991b01 100644 --- a/src/dropin.js +++ b/src/dropin.js @@ -876,10 +876,16 @@ Dropin.prototype.requestPaymentMethod = function (options) { self._mainView.showLoadingIndicator(); return self._threeDSecure.verify(payload, options.threeDSecure).then(function (newPayload) { + self._model.shouldWaitForVerifyCard = false; payload.nonce = newPayload.nonce; payload.liabilityShifted = newPayload.liabilityShifted; payload.liabilityShiftPossible = newPayload.liabilityShiftPossible; payload.threeDSecureInfo = newPayload.threeDSecureInfo; + self._model.setPaymentMethodRequestable({ + isRequestable: Boolean(newPayload), + type: newPayload.type, + selectedPaymentMethod: payload + }); self._mainView.hideLoadingIndicator(); diff --git a/src/lib/three-d-secure.js b/src/lib/three-d-secure.js index d8d74943..38fc6cdd 100644 --- a/src/lib/three-d-secure.js +++ b/src/lib/three-d-secure.js @@ -54,6 +54,7 @@ ThreeDSecure.prototype.verify = function (payload, merchantProvidedData) { verifyOptions.additionalInformation = verifyOptions.additionalInformation || {}; verifyOptions.additionalInformation.acsWindowSize = verifyOptions.additionalInformation.acsWindowSize || DEFAULT_ACS_WINDOW_SIZE; + this._model.shouldWaitForVerifyCard = true; return this._instance.verifyCard(verifyOptions); }; diff --git a/test/unit/dropin-model.js b/test/unit/dropin-model.js index 486ac938..ca83e522 100644 --- a/test/unit/dropin-model.js +++ b/test/unit/dropin-model.js @@ -1084,6 +1084,31 @@ describe('DropinModel', () => { } ); + test( + 'does not emit paymentMethodRequestable event until after three D secure verification has been completed', + () => { + testContext.model.shouldWaitForVerifyCard = true; + testContext.model.setPaymentMethodRequestable({ + isRequestable: true, + type: 'card' + }); + + expect(testContext.model._emit).not.toBeCalled(); + + testContext.model.shouldWaitForVerifyCard = false; + + testContext.model.setPaymentMethodRequestable({ + isRequestable: true, + type: 'card', + selectedPaymentMethod: { + nonce: 'fake-nonce' + } + }); + + expect(testContext.model._emit).toBeCalled(); + } + ); + test( 'sets isPaymentMethodRequestable to false when isRequestable is false', () => { diff --git a/test/unit/dropin.js b/test/unit/dropin.js index f464326f..e1694aaf 100644 --- a/test/unit/dropin.js +++ b/test/unit/dropin.js @@ -1212,6 +1212,43 @@ describe('Dropin', () => { } ); + test('sets shouldWaitForVerifyCard to false and calls setPaymentMethodRequestable when 3D secure is complete', done => { + let instance; + const fakePayload = { + nonce: 'cool-nonce', + type: 'CreditCard' + }; + const fakeNewPayload = { + nonce: 'new-nonce', + liabilityShifted: true, + liabilityShiftPossible: true, + type: fakePayload.type + }; + + testContext.dropinOptions.merchantConfiguration.threeDSecure = {}; + + instance = new Dropin(testContext.dropinOptions); + + instance._initialize(() => { + jest.spyOn(instance._mainView, 'requestPaymentMethod').mockResolvedValue(fakePayload); + jest.spyOn(instance._model, 'setPaymentMethodRequestable').mockResolvedValue(); + instance._threeDSecure = { + verify: jest.fn().mockResolvedValue(fakeNewPayload) + }; + + instance.requestPaymentMethod(() => { + expect(instance._model.shouldWaitForVerifyCard).toBe(false); + expect(instance._model.setPaymentMethodRequestable).toBeCalledWith({ + isRequestable: true, + type: fakeNewPayload.type, + selectedPaymentMethod: fakeNewPayload + }); + + done(); + }); + }); + }); + test( 'does not call 3D Secure if network tokenized google pay', done => { diff --git a/test/unit/lib/three-d-secure.js b/test/unit/lib/three-d-secure.js index d82bb7cb..8b3444e0 100644 --- a/test/unit/lib/three-d-secure.js +++ b/test/unit/lib/three-d-secure.js @@ -119,6 +119,19 @@ describe('ThreeDSecure', () => { }); }); + test('sets shouldWaitForVerifyCard to true', () => { + expect(testContext.model.shouldWaitForVerifyCard).toBe(false); + + return testContext.tds.verify({ + nonce: 'old-nonce', + details: { + bin: '123456' + } + }).then(() => { + expect(testContext.model.shouldWaitForVerifyCard).toBe(true); + }); + }); + test('rejects if verifyCard rejects', () => { testContext.threeDSecureInstance.verifyCard.mockRejectedValue({ message: 'A message'