Skip to content

Commit

Permalink
chore: extract loading braintree libraries to util/braintree
Browse files Browse the repository at this point in the history
Consolidates loading Braintree dependencies into a shared module instead
of copying it around each module that has a Braintree integration.

```js
import BraintreeClient from 'path/to/./util/braintree';

BraintreeClient.loadModules('applePay', 'dataCollector')
  .catch(...)
  .then(...)
```
  • Loading branch information
cbarton committed Aug 14, 2024
1 parent 1ea4ec5 commit ccbebb6
Show file tree
Hide file tree
Showing 12 changed files with 69 additions and 159 deletions.
1 change: 0 additions & 1 deletion lib/const/gateway-constants.js

This file was deleted.

29 changes: 2 additions & 27 deletions lib/recurly/apple-pay/apple-pay.braintree.js
Original file line number Diff line number Diff line change
@@ -1,44 +1,19 @@
import Promise from 'promise';
import { ApplePay } from './apple-pay';
import loadScriptPromise from '../../util/load-script-promise';
import Debug from 'debug';
import { BRAINTREE_CLIENT_VERSION } from '../../const/gateway-constants';
import BraintreeClient from '../../util/braintree-client';

const debug = Debug('recurly:apple-pay:braintree');

const LIBS = {
client: 'client',
applePay: 'apple-pay',
dataCollector: 'data-collector',
};

const loadBraintree = (...libs) => {
const loadLib = lib => {
const isLibPresent = window.braintree?.client?.VERSION === BRAINTREE_CLIENT_VERSION &&
lib in window.braintree;

return isLibPresent
? Promise.resolve()
: loadScriptPromise(ApplePayBraintree.libUrl(lib));
};

return loadLib('client')
.then(() => Promise.all(libs.map(loadLib)));
};

export class ApplePayBraintree extends ApplePay {
static libUrl (lib) {
return `https://js.braintreegateway.com/web/${BRAINTREE_CLIENT_VERSION}/js/${LIBS[lib]}.min.js`;
}

configure (options) {
debug('Initializing client');

const authorization = options.braintree.clientAuthorization;
if (options.braintree.displayName) this.displayName = options.braintree.displayName;
else this.displayName = 'My Store';

loadBraintree('applePay', 'dataCollector')
BraintreeClient.loadModules('applePay', 'dataCollector')
.then(() => window.braintree.client.create({ authorization }))
.then(client => Promise.all([
window.braintree.dataCollector.create({ client }),
Expand Down
44 changes: 4 additions & 40 deletions lib/recurly/paypal/strategy/braintree.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import loadScript from 'load-script';
import after from '../../../util/after';
import BraintreeClient from '../../../util/braintree-client';
import { PayPalStrategy } from './index';
import { BRAINTREE_CLIENT_VERSION } from '../../../const/gateway-constants';

const debug = require('debug')('recurly:paypal:strategy:braintree');

Expand All @@ -10,45 +8,16 @@ const debug = require('debug')('recurly:paypal:strategy:braintree');
*/

export class BraintreeStrategy extends PayPalStrategy {
constructor (...args) {
super(...args);
this.load();
}

configure (options) {
super.configure(options);
if (!options.braintree || !options.braintree.clientAuthorization) {
throw this.error('paypal-config-missing', { opt: 'braintree.clientAuthorization' });
}
this.config.clientAuthorization = options.braintree.clientAuthorization;
}

/**
* Loads Braintree client and modules
*
* @todo semver client detection
*/
load () {
debug('loading Braintree libraries');

const part = after(2, () => this.initialize());
const get = (lib, done = () => {}) => {
const uri = `https://js.braintreegateway.com/web/${BRAINTREE_CLIENT_VERSION}/js/${lib}.min.js`;
loadScript(uri, error => {
if (error) this.error('paypal-load-error', { cause: error });
else done();
});
};

const modules = () => {
if (this.braintreeClientAvailable('paypal')) part();
else get('paypal', part);
if (this.braintreeClientAvailable('dataCollector')) part();
else get('data-collector', part);
};

if (this.braintreeClientAvailable()) modules();
else get('client', modules);
BraintreeClient.loadModules('paypal', 'dataCollector')
.catch(cause => this.error('paypal-load-error', { cause }))
.then(() => this.initialize());
}

/**
Expand Down Expand Up @@ -113,9 +82,4 @@ export class BraintreeStrategy extends PayPalStrategy {
}
this.off();
}

braintreeClientAvailable (module) {
const bt = window.braintree;
return bt && bt.client && bt.client.VERSION === BRAINTREE_CLIENT_VERSION && (module ? module in bt : true);
}
}
30 changes: 5 additions & 25 deletions lib/recurly/risk/three-d-secure/strategy/braintree.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
import loadScript from 'load-script';
import Promise from 'promise';
import BraintreeClient from '../../../../util/braintree-client';
import ThreeDSecureStrategy from './strategy';
import { BRAINTREE_CLIENT_VERSION } from '../../../../const/gateway-constants';

const debug = require('debug')('recurly:risk:three-d-secure:braintree');

export default class BraintreeStrategy extends ThreeDSecureStrategy {

static strategyName = 'braintree_blue';

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

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

debug('loading braintree libraries');
this.loadBraintreeLibraries()
.catch(cause => this.threeDSecure.error('3ds-vendor-load-error', { vendor: 'Braintree', cause }))
.then(() => {
this.braintree = window.braintree;
debug('Braintree checkout instance created', this.braintree);
this.markReady();
});
}
Expand Down Expand Up @@ -97,24 +97,4 @@ export default class BraintreeStrategy extends ThreeDSecureStrategy {
.catch(cause => this.threeDSecure.error('3ds-auth-error', { cause }));
});
}

urlForResource (type) {
return `https://js.braintreegateway.com/web/${BRAINTREE_CLIENT_VERSION}/js/${type}.min.js`;
}

/**
* Loads Braintree library dependency
*/
loadBraintreeLibraries () {
return new Promise((resolve, reject) => {
if (window.braintree && window.braintree.client && window.braintree.threeDSecure) return resolve();
loadScript(this.urlForResource('client'), error => {
if (error) reject(error);
else loadScript(this.urlForResource('three-d-secure'), error => {
if (error) reject(error);
else resolve();
});
});
});
}
}
46 changes: 6 additions & 40 deletions lib/recurly/venmo/strategy/braintree.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import loadScript from 'load-script';
import after from '../../../util/after';
import BraintreeClient from '../../../util/braintree-client';
import { VenmoStrategy } from './index';
import { normalize } from '../../../util/normalize';
import { BRAINTREE_CLIENT_VERSION } from '../../../const/gateway-constants';

const debug = require('debug')('recurly:venmo:strategy:braintree');

Expand All @@ -11,47 +9,20 @@ const debug = require('debug')('recurly:venmo:strategy:braintree');
*/

export class BraintreeStrategy extends VenmoStrategy {
constructor (...args) {
super(args);
this.load(args[0]);
}

configure (options) {
super.configure(options);

if (!options.braintree || !options.braintree.clientAuthorization) {
throw this.error('venmo-config-missing', { opt: 'braintree.clientAuthorization' });
}
this.config.clientAuthorization = options.braintree.clientAuthorization;
this.config.allowDesktopWebLogin = options.braintree.webAuthentication ? options.braintree.webAuthentication : false;
}

/**
* Loads Braintree client and modules
*
* @todo semver client detection
*/
load ({ form }) {
debug('loading Braintree libraries');
this.form = form;
this.form = options.form;

const part = after(2, () => this.initialize());
const get = (lib, done = () => {}) => {
const uri = `https://js.braintreegateway.com/web/${BRAINTREE_CLIENT_VERSION}/js/${lib}.min.js`;
loadScript(uri, error => {
if (error) this.error('venmo-load-error', { cause: error });
else done();
});
};

const modules = () => {
if (this.braintreeClientAvailable('venmo')) part();
else get('venmo', part);
if (this.braintreeClientAvailable('dataCollector')) part();
else get('data-collector', part);
};

if (this.braintreeClientAvailable()) modules();
else get('client', modules);
BraintreeClient.loadModules('venmo', 'dataCollector')
.catch(cause => this.error('venmo-load-error', { cause }))
.then(() => this.initialize());
}

/**
Expand Down Expand Up @@ -124,9 +95,4 @@ export class BraintreeStrategy extends VenmoStrategy {
}
this.off();
}

braintreeClientAvailable (module) {
const bt = window.braintree;
return bt && bt.client && bt.client.VERSION === BRAINTREE_CLIENT_VERSION && (module ? module in bt : true);
}
}
2 changes: 1 addition & 1 deletion lib/recurly/venmo/strategy/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export class VenmoStrategy extends Emitter {

this.once('ready', () => this.isReady = true);

this.configure(options[0]);
this.configure(options);
}

ready (done) {
Expand Down
11 changes: 0 additions & 11 deletions lib/util/after.js

This file was deleted.

37 changes: 37 additions & 0 deletions lib/util/braintree-client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import loadScriptPromise from './load-script-promise';
import Debug from 'debug';

const debug = Debug('recurly:braintree');

const BRAINTREE_CLIENT_VERSION = '3.101.0';

const MOD_TO_LIB = {
'dataCollector': 'data-collector',
'applePay': 'apple-pay',
'googlePayment': 'google-payment',
'threeDSecure': 'three-d-secure',
};

const libUrl = (mod) => {
const btMod = MOD_TO_LIB[mod] || mod;
return `https://js.braintreegateway.com/web/${BRAINTREE_CLIENT_VERSION}/js/${btMod}.min.js`;
};

const loadModuleScript = (mod) => {
const isModulePresent = window.braintree?.client?.VERSION === BRAINTREE_CLIENT_VERSION && mod in window.braintree;

return isModulePresent
? Promise.resolve()
: loadScriptPromise(libUrl(mod));
};

export default {
BRAINTREE_CLIENT_VERSION,

loadModules: (...modules) => {
debug('loading Braintree client modules', modules);

return loadModuleScript('client')
.then(() => Promise.all(modules.map(loadModuleScript)));
},
};
10 changes: 4 additions & 6 deletions test/unit/paypal/strategy/braintree.test.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,21 @@
import assert from 'assert';
import each from 'component-each';
import merge from 'lodash.merge';
import { Recurly } from '../../../../lib/recurly';
import {
initRecurly,
stubBraintree,
stubWindowOpen
} from '../../support/helpers';

describe(`BraintreeStrategy`, function () {
describe('BraintreeStrategy', function () {
const validOpts = { braintree: { clientAuthorization: 'valid' } };

stubWindowOpen();
stubBraintree();

beforeEach(function () {
beforeEach(function (done) {
this.sandbox = sinon.createSandbox();
this.recurly = initRecurly();
this.paypal = this.recurly.PayPal(validOpts);
this.paypal.on('ready', done);
});

describe('start', function () {
Expand All @@ -26,7 +24,7 @@ describe(`BraintreeStrategy`, function () {
this.paypal.start();
assert(this.paypal.strategy.paypal.tokenize.calledOnce);
});
})
});

describe('destroy', function () {
it('closes the window and removes listeners', function () {
Expand Down
7 changes: 4 additions & 3 deletions test/unit/risk/three-d-secure/strategy/braintree.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import assert from 'assert';
import { applyFixtures } from '../../../support/fixtures';
import { initRecurly, testBed } from '../../../support/helpers';
import BraintreeStrategy from '../../../../../lib/recurly/risk/three-d-secure/strategy/braintree';
import BraintreeClient from '../../../../../lib/util/braintree-client';
import actionToken from '@recurly/public-api-test-server/fixtures/tokens/action-token-braintree.json';
import Promise from 'promise';

describe('BraintreeStrategy', function () {
this.ctx.fixture = 'threeDSecure';
Expand All @@ -25,6 +25,7 @@ describe('BraintreeStrategy', function () {
};
this.braintree = {
client: {
VERSION: BraintreeClient.BRAINTREE_CLIENT_VERSION,
create: sinon.stub().resolves()
},
threeDSecure: {
Expand All @@ -46,7 +47,7 @@ describe('BraintreeStrategy', function () {
describe('when the braintree.js library encounters a load error', function () {
beforeEach(function () {
const { sandbox, threeDSecure } = this;
sandbox.replace(BraintreeStrategy.prototype, 'urlForResource', (f) => '/api/mock-404');
sandbox.stub(BraintreeClient, 'loadModules').rejects();
delete window.braintree;
this.strategy = new BraintreeStrategy({ threeDSecure, actionToken });
});
Expand All @@ -57,7 +58,7 @@ describe('BraintreeStrategy', function () {
assert.strictEqual(error.code, '3ds-vendor-load-error');
assert.strictEqual(error.vendor, 'Braintree');
done();
})
});
});
});

Expand Down
4 changes: 2 additions & 2 deletions test/unit/support/helpers.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import bowser from 'bowser';
import merge from 'lodash.merge';
import { Recurly } from '../../../lib/recurly';
import { BRAINTREE_CLIENT_VERSION } from '../../../lib/const/gateway-constants';
import BraintreeClient from '../../../lib/util/braintree-client';

import Promise from 'promise';

Expand Down Expand Up @@ -62,7 +62,7 @@ export function stubBraintree () {

window.braintree = {
client: {
VERSION: BRAINTREE_CLIENT_VERSION,
VERSION: BraintreeClient.BRAINTREE_CLIENT_VERSION,
create
},
venmo: {
Expand Down
Loading

0 comments on commit ccbebb6

Please sign in to comment.