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

chore: extract loading braintree libraries to util/braintree #891

Merged
merged 2 commits into from
Aug 14, 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
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 BraintreeLoader from '../../util/braintree-loader';

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')
BraintreeLoader.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 BraintreeLoader from '../../../util/braintree-loader';
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);
BraintreeLoader.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 BraintreeLoader from '../../../../util/braintree-loader';
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 BraintreeLoader.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 BraintreeLoader from '../../../util/braintree-loader';
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);
BraintreeLoader.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-loader.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)));
},
};
54 changes: 7 additions & 47 deletions test/unit/apple-pay.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import omit from 'lodash.omit';
import Emitter from 'component-emitter';
import Promise from 'promise';
import { initRecurly, nextTick } from './support/helpers';
import BraintreeLoader from '../../lib/util/braintree-loader';
import { ApplePayBraintree } from '../../lib/recurly/apple-pay/apple-pay.braintree';
import filterSupportedNetworks from '../../lib/recurly/apple-pay/util/filter-supported-networks';

Expand Down Expand Up @@ -78,9 +79,7 @@ const getBraintreeStub = () => ({
},
});

const maybeDescribe = 'ApplePaySession' in window ? describe : describe.skip;

maybeDescribe('ApplePay', function () {
describe('ApplePay', function () {
beforeEach(function () {
this.sandbox = sinon.createSandbox();
window.ApplePaySession = ApplePaySessionStub;
Expand Down Expand Up @@ -719,58 +718,19 @@ function applePayTest (integrationType) {
describe('when the libs are not loaded', function () {
beforeEach(function () {
delete window.braintree;
this.sandbox.stub(ApplePayBraintree, 'libUrl').returns('/api/mock-200');
this.sandbox.stub(BraintreeLoader, 'loadModules').rejects('boom');
});

it('load the libs', function (done) {
const applePay = this.recurly.ApplePay(validOpts);
applePay.on('error', ensureDone(done, () => {
assert.equal(ApplePayBraintree.libUrl.callCount, 3);
assert.equal(ApplePayBraintree.libUrl.getCall(0).args[0], 'client');
assert.equal(ApplePayBraintree.libUrl.getCall(1).args[0], 'applePay');
assert.equal(ApplePayBraintree.libUrl.getCall(2).args[0], 'dataCollector');
applePay.on('error', ensureDone(done, (err) => {
assert(BraintreeLoader.loadModules.calledWith('applePay', 'dataCollector'));
assert.equal(err, applePay.initError);
assertInitError(applePay, 'apple-pay-init-error');
}));
});
});

const requiredBraintreeLibs = ['client', 'dataCollector', 'applePay'];
requiredBraintreeLibs.forEach(requiredLib => {
describe(`when failed to load the braintree ${requiredLib} lib`, function () {
beforeEach(function () {
delete window.braintree;
this.sandbox.stub(ApplePayBraintree, 'libUrl').withArgs(requiredLib).returns('/api/mock-404');
});

it('register an initialization error', function (done) {
const applePay = this.recurly.ApplePay(validOpts);

applePay.on('error', (err) => {
nextTick(ensureDone(done, () => {
assert.equal(err, applePay.initError);
assertInitError(applePay, 'apple-pay-init-error');
}));
});
});
});

describe(`when failed to create the ${requiredLib} instance`, function () {
beforeEach(function () {
window.braintree[requiredLib].create = sinon.stub().rejects('error');
});

it('register an initialization error', function (done) {
const applePay = this.recurly.ApplePay(validOpts);

applePay.on('error', (err) => {
nextTick(ensureDone(done, () => {
assert.equal(err, applePay.initError);
assertInitError(applePay, 'apple-pay-init-error');
}));
});
});
});
});

it('assigns the braintree configuration', function (done) {
const applePay = this.recurly.ApplePay(validOpts);

Expand Down
Loading
Loading