Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Emit paymentMethodRequestable event after 3DS Challenge is completed #917

Merged
merged 10 commits into from
Dec 20, 2023
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
9 changes: 9 additions & 0 deletions src/dropin-model.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -98,6 +99,10 @@ DropinModel.prototype.confirmDropinReady = function () {
this._setupComplete = true;
};

DropinModel.prototype.verifyCardReady = function () {
jplukarski marked this conversation as resolved.
Show resolved Hide resolved
this._shouldWaitForVerifyCard = !this._shouldWaitForVerifyCard;
jplukarski marked this conversation as resolved.
Show resolved Hide resolved
};

DropinModel.prototype.isPaymentMethodRequestable = function () {
return Boolean(this._paymentMethodIsRequestable);
};
Expand Down Expand Up @@ -223,6 +228,10 @@ DropinModel.prototype._shouldEmitRequestableEvent = function (options) {
return false;
}

if (this._shouldWaitForVerifyCard) {
return false;
}

if (requestableStateHasNotChanged && (!options.isRequestable || nonceHasNotChanged)) {
return false;
}
Expand Down
6 changes: 6 additions & 0 deletions src/dropin.js
Original file line number Diff line number Diff line change
Expand Up @@ -876,10 +876,16 @@ Dropin.prototype.requestPaymentMethod = function (options) {
self._mainView.showLoadingIndicator();

return self._threeDSecure.verify(payload, options.threeDSecure).then(function (newPayload) {
self._model.verifyCardReady();
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();

Expand Down
1 change: 1 addition & 0 deletions src/lib/three-d-secure.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
}, merchantProvidedData, {
nonce: payload.nonce,
bin: payload.details.bin,
// TODO in the future, we will allow

Check warning on line 47 in src/lib/three-d-secure.js

View workflow job for this annotation

GitHub Actions / Unit Tests and Linter

Unexpected 'todo' comment: 'TODO in the future, we will allow'
// merchants to pass in a custom
// onLookupComplete hook
onLookupComplete: function (data, next) {
Expand All @@ -54,6 +54,7 @@

verifyOptions.additionalInformation = verifyOptions.additionalInformation || {};
verifyOptions.additionalInformation.acsWindowSize = verifyOptions.additionalInformation.acsWindowSize || DEFAULT_ACS_WINDOW_SIZE;
this._model.verifyCardReady();
jplukarski marked this conversation as resolved.
Show resolved Hide resolved

return this._instance.verifyCard(verifyOptions);
};
Expand Down
41 changes: 41 additions & 0 deletions test/unit/dropin-model.js
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,22 @@ describe('DropinModel', () => {
});
});

describe('verifyCardReady', () => {
test('toggles _shouldWaitForVerifyCard from true to false', () => {
const model = new DropinModel(testContext.modelOptions);

expect(model._shouldWaitForVerifyCard).toBe(false);

model.verifyCardReady();

expect(model._shouldWaitForVerifyCard).toBe(true);

model.verifyCardReady();

expect(model._shouldWaitForVerifyCard).toBe(false);
});
});

describe('initialize', () => {
test('emits asyncDependenciesReady event when no dependencies are set to initializing', (done) => {
jest.useFakeTimers();
Expand Down Expand Up @@ -1084,6 +1100,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',
() => {
Expand Down
38 changes: 38 additions & 0 deletions test/unit/dropin.js
Original file line number Diff line number Diff line change
Expand Up @@ -1212,6 +1212,44 @@ describe('Dropin', () => {
}
);

test('calls verifyCardReady and setPaymentMethodRequestable when 3D secure is called', done => {
jplukarski marked this conversation as resolved.
Show resolved Hide resolved
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, 'verifyCardReady').mockResolvedValue();
jest.spyOn(instance._model, 'setPaymentMethodRequestable').mockResolvedValue();
instance._threeDSecure = {
verify: jest.fn().mockResolvedValue(fakeNewPayload)
};

instance.requestPaymentMethod(() => {
expect(instance._model.verifyCardReady).toBeCalled();
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 => {
Expand Down
13 changes: 13 additions & 0 deletions test/unit/lib/three-d-secure.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,19 @@ describe('ThreeDSecure', () => {
});
});

test('calls verifyCardReady', () => {
jest.spyOn(testContext.model, 'verifyCardReady').mockResolvedValue();

return testContext.tds.verify({
nonce: 'old-nonce',
details: {
bin: '123456'
}
}).then(() => {
expect(testContext.model.verifyCardReady).toBeCalled();
});
});

test('rejects if verifyCard rejects', () => {
testContext.threeDSecureInstance.verifyCard.mockRejectedValue({
message: 'A message'
Expand Down
Loading