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

Proactive 3DS changes #896

Merged
merged 1 commit into from
Sep 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion lib/recurly.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,13 @@ const DEFAULTS = {
},
report: false,
risk: {
threeDSecure: { preflightDeviceDataCollector: true }
threeDSecure: {
preflightDeviceDataCollector: true,
proactive: {
enabled: false,
gilv93 marked this conversation as resolved.
Show resolved Hide resolved
gatewayCode: ''
}
}
},
api: DEFAULT_API_URL,
fields: {
Expand Down
28 changes: 19 additions & 9 deletions lib/recurly/risk/risk.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,18 +55,28 @@ export class Risk {
* @param {String} options.bin credit card BIN
* @return {Promise}
*/
static preflight ({ recurly, number, month, year }) {
return recurly.request.get({ route: '/risk/preflights' })
static preflight ({ recurly, number, month, year, cvv }) {
const data = {};

if (recurly.config.risk.threeDSecure.proactive.enabled) {
data.proactive = true;
data.gateway_code = recurly.config.risk.threeDSecure.proactive.gatewayCode;
}

return recurly.request.get({ route: '/risk/preflights', data })
.then(({ preflights }) => {
debug('received preflight instructions', preflights);
return ThreeDSecure.preflight({ recurly, number, month, year, preflights });
return ThreeDSecure.preflight({ recurly, number, month, year, cvv, preflights });
})
.then(results => results.filter(maybeErr => {
if (maybeErr.code === 'risk-preflight-timeout') {
debug('timeout encountered', maybeErr);
return false;
}
return true;
.then(({ tokenType, risk }) => ({
risk: risk.filter(maybeErr => {
if (maybeErr.code === 'risk-preflight-timeout') {
debug('timeout encountered', maybeErr);
return false;
}
return true;
}),
tokenType
}));
}

Expand Down
38 changes: 34 additions & 4 deletions lib/recurly/risk/three-d-secure/strategy/braintree.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,45 @@ import ThreeDSecureStrategy from './strategy';
const debug = require('debug')('recurly:risk:three-d-secure:braintree');

export default class BraintreeStrategy extends ThreeDSecureStrategy {

static strategyName = 'braintree_blue';

loadBraintreeLibraries () {
return BraintreeLoader.loadModules('threeDSecure');
}

static preflight ({ recurly, number, month, year, cvv }) {
const { enabled, gatewayCode, amount } = recurly.config.risk.threeDSecure.proactive;

debug('performing preflight for', { gatewayCode });

if (!enabled) {
return Promise.resolve();
}

const data = {
gateway_type: BraintreeStrategy.strategyName,
gateway_code: gatewayCode,
number,
month,
year,
cvv
};

// we don't really need to do anything once we get a response except
// resolve with relevant data instead of session_id
return recurly.request.post({ route: '/risk/authentications', data })
.then(({ paymentMethodNonce, clientToken, bin }) => ({
results: {
payment_method_nonce: paymentMethodNonce,
client_token: clientToken,
bin,
amount: amount
},
tokenType: 'three_d_secure_proactive_action'
}));
}


constructor (...args) {
super(...args);

Expand All @@ -31,7 +63,7 @@ export default class BraintreeStrategy extends ThreeDSecureStrategy {
}

get amount () {
return this.actionToken.transaction.amount;
return this.actionToken.transaction?.amount || this.actionToken.three_d_secure.amount;
}

get billingInfo () {
Expand All @@ -54,9 +86,7 @@ export default class BraintreeStrategy extends ThreeDSecureStrategy {

this.whenReady(() => {
debug('Attempting to load braintree');

const { braintree, braintreeClientToken, amount, nonce, bin, billingInfo } = this;

const verifyCardOptions = {
amount: amount,
nonce: nonce,
Expand Down
2 changes: 1 addition & 1 deletion lib/recurly/risk/three-d-secure/strategy/cybersource.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export default class CybersourceStrategy extends ThreeDSecureStrategy {
const body = JSON.parse(data);
if (body.MessageType === 'profile.completed') {
debug('received device data session id', body);
resolve({ session_id: body.SessionId });
resolve({ results: { session_id: body.SessionId } });
frame.destroy();
recurly.bus.off('raw-message', listener);
}
Expand Down
4 changes: 2 additions & 2 deletions lib/recurly/risk/three-d-secure/strategy/strategy.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ export default class ThreeDSecureStrategy extends ReadinessEmitter {
static preflight () {}
static PREFLIGHT_TIMEOUT = 30000;

constructor ({ threeDSecure, actionToken }) {
constructor ({ threeDSecure, actionToken, proactiveToken }) {
super();
this.threeDSecure = threeDSecure;
this.actionToken = actionToken;
this.actionToken = actionToken || proactiveToken;
}

get strategyName () {
Expand Down
2 changes: 1 addition & 1 deletion lib/recurly/risk/three-d-secure/strategy/worldpay.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export default class WorldpayStrategy extends ThreeDSecureStrategy {
const body = JSON.parse(data);
if (body.MessageType === 'profile.completed') {
debug('received device data session id', body);
resolve({ session_id: body.SessionId });
resolve({ results: { session_id: body.SessionId } });
recurly.bus.off('raw-message', listener);
frame.destroy();
}
Expand Down
36 changes: 27 additions & 9 deletions lib/recurly/risk/three-d-secure/three-d-secure.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ export class ThreeDSecure extends RiskConcern {
'05': { height: '100%', width: '100%' }
}

static VALID_ACTION_TOKEN_TYPES = [
'three_d_secure_action',
'three_d_secure_proactive_action'
];

/**
* Returns a strateggy for a given gateway type
*
Expand All @@ -94,18 +99,26 @@ export class ThreeDSecure extends RiskConcern {
* @param {Preflights} options.preflights
* @return {Promise}
*/
static preflight ({ recurly, number, month, year, preflights }) {
static preflight ({ recurly, number, month, year, cvv, preflights }) {
return preflights.reduce((preflight, result) => {
return preflight.then((finishedPreflights) => {
const { type } = result.gateway;
const { type: gatewayType } = result.gateway;
const { gateway_code } = result.params;
const strategy = ThreeDSecure.getStrategyForGatewayType(type);
return strategy.preflight({ recurly, number, month, year, ...result.params })
.then(results => {
return finishedPreflights.concat([{ processor: type, gateway_code, results }]);
const strategy = ThreeDSecure.getStrategyForGatewayType(gatewayType);
return strategy.preflight({ recurly, number, month, year, cvv, ...result.params })
.then(({ results, tokenType }) => {
// return finishedPreflights.concat([{ processor: type, gateway_code, results}]);
return {
tokenType: finishedPreflights.tokenType || tokenType,
risk: finishedPreflights.risk.concat({
processor: gatewayType,
gateway_code,
results
})
};
});
});
}, Promise.resolve([]));
}, Promise.resolve({ risk: [] }));
}

constructor ({ risk, actionTokenId, challengeWindowSize }) {
Expand Down Expand Up @@ -183,6 +196,7 @@ export class ThreeDSecure extends RiskConcern {
three_d_secure_action_token_id: this.actionTokenId,
results
};

debug('submitting results for tokenization', data);
return this.recurly.request.post({ route: '/tokens', data });
}
Expand Down Expand Up @@ -219,6 +233,10 @@ export class ThreeDSecure extends RiskConcern {
}

function assertIsActionToken (token) {
if (token && token.type === 'three_d_secure_action') return;
throw errors('invalid-option', { name: 'actionTokenId', expect: 'a three_d_secure_action_token_id' });
if (ThreeDSecure.VALID_ACTION_TOKEN_TYPES.includes(token?.type)) return;

throw errors('invalid-option', {
name: 'actionTokenId',
expect: `a token of type: ${ThreeDSecure.VALID_ACTION_TOKEN_TYPES.join(',')}`
});
}
9 changes: 6 additions & 3 deletions lib/recurly/token.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,12 @@ function token (customerData, bus, done) {
}));
}

const { number, month, year } = inputs;
Risk.preflight({ recurly: this, number, month, year })
.then(results => inputs.risk = results)
const { number, month, year, cvv } = inputs;
Risk.preflight({ recurly: this, number, month, year, cvv })
.then(({ risk, tokenType }) => {
inputs.risk = risk;
if (tokenType) inputs.type = tokenType;
})
.then(() => this.request.post({ route: '/token', data: inputs, done: complete }))
.done();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"type": "three_d_secure_proactive_action",
"id": "proactive-token-test",
"gateway": {
"code": "1234567890",
"type": "test"
},
"three_d_secure": {
"params": {
"challengeType": "challenge"
}
}
}
23 changes: 23 additions & 0 deletions test/unit/recurly.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,29 @@ describe('Recurly', function () {
});
});
});

describe('when proactive3ds', function () {
describe('is set to true', function() {
it('returns true', function () {
const recurly = initRecurly({
risk: {
threeDSecure: {
proactive: {
enabled: true
}
}
}
});
assert.strictEqual(recurly.config.risk.threeDSecure.proactive.enabled, true);
});
});
describe('is not set', function() {
it('returns false', function () {
const recurly = initRecurly({});
assert.strictEqual(recurly.config.risk.threeDSecure.proactive.enabled, false);
});
})
});
});

describe('destroy', function () {
Expand Down
12 changes: 6 additions & 6 deletions test/unit/risk.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ describe('Risk', function () {
const { sandbox, recurly } = this;
this.bin = '411111';
this.recurly = initRecurly({ publicKey: 'test-preflight-key' });
this.stubPreflightResults = [{ arbitrary: 'preflight-results' }];
this.stubPreflightResults = { risk: [{ arbitrary: 'results' }], tokenType: undefined };
sandbox.stub(ThreeDSecure, 'preflight').usingPromise(Promise).resolves(this.stubPreflightResults);
});

Expand All @@ -87,22 +87,22 @@ describe('Risk', function () {

describe('when some results are timeouts', function () {
beforeEach(function () {
this.stubPreflightResults = [
this.stubPreflightResults = { risk: [
{ arbitrary: 'preflight-results' },
errors('risk-preflight-timeout', { processor: 'test' }),
{ arbitrary: 'preflight-results-2' },
errors('risk-preflight-timeout', { processor: 'test-2' })
];
], tokenType: undefined};
ThreeDSecure.preflight.usingPromise(Promise).resolves(this.stubPreflightResults);
});

it('filters out those timeout results', function (done) {
const { recurly, bin, stubPreflightResults } = this;
Risk.preflight({ recurly, bin })
.done(results => {
assert.strictEqual(results.length, 2);
assert.deepStrictEqual(results[0], stubPreflightResults[0]);
assert.deepStrictEqual(results[1], stubPreflightResults[2]);
assert.strictEqual(results.risk.length, 2);
assert.deepStrictEqual(results.risk[0], stubPreflightResults.risk[0]);
assert.deepStrictEqual(results.risk[1], stubPreflightResults.risk[2]);
done();
});
});
Expand Down
8 changes: 4 additions & 4 deletions test/unit/risk/three-d-secure.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ describe('ThreeDSecure', function () {
}
];
sandbox.stub(ThreeDSecure, 'getStrategyForGatewayType').callsFake(() => ({
preflight: sandbox.stub().usingPromise(Promise).resolves({ arbitrary: 'test-results' })
preflight: sandbox.stub().usingPromise(Promise).resolves({ results: { arbitrary: 'test-results' } })
}));
});

Expand All @@ -108,9 +108,9 @@ describe('ThreeDSecure', function () {
it('resolves with preflight results from strategies', function (done) {
const { recurly, bin, preflights } = this;
const returnValue = ThreeDSecure.preflight({ recurly, bin, preflights })
.done(response => {
const [{ processor, results }] = response;
assert.strictEqual(Array.isArray(response), true);
.done(({ risk }) => {
const [{ processor, results }] = risk;
assert.strictEqual(Array.isArray(risk), true);
assert.strictEqual(processor, 'test-gateway-type');
assert.deepStrictEqual(results, { arbitrary: 'test-results' });
done();
Expand Down
2 changes: 1 addition & 1 deletion test/unit/risk/three-d-secure/strategy/cybersource.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ describe('CybersourceStrategy', function () {
const { recurly, Strategy, sessionId, number, month, year, gateway_code, jwt, poll } = this;

Strategy.preflight({ recurly, number, month, year, gateway_code }).then(preflightResponse => {
assert.strictEqual(preflightResponse.session_id, sessionId);
assert.strictEqual(preflightResponse.results.session_id, sessionId);

clearInterval(poll);
done();
Expand Down
2 changes: 1 addition & 1 deletion test/unit/risk/three-d-secure/strategy/worldpay.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ describe('WorldpayStrategy', function () {
const { recurly, Strategy, number, jwt, deviceDataCollectionUrl, sessionId, simulatePreflightResponse } = this;

Strategy.preflight({ recurly, number, jwt, deviceDataCollectionUrl }).then(preflightResponse => {
assert.strictEqual(preflightResponse.session_id, sessionId);
assert.strictEqual(preflightResponse.results.session_id, sessionId);
done();
});

Expand Down
4 changes: 4 additions & 0 deletions types/lib/configure.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ export type RecurlyOptions = {
risk?: {
threeDSecure?: {
preflightDeviceDataCollector?: boolean;
proactive?: {
enabled: true;
gatewayCode: string;
}
}
};

Expand Down
Loading